OpenFeign / querydsl

Unified Queries for Java
https://querydsl.com
Apache License 2.0
260 stars 39 forks source link

Added support for deserializing object property values based on value class #510

Closed quicklyfast closed 3 months ago

quicklyfast commented 3 months ago

Add the getValue method in Type to deserialize JSON data from a ResultSet into an object of the specified class (clazz).

package com.example;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.querydsl.core.types.Path;
import com.querydsl.sql.Configuration;
import com.querydsl.sql.MySQLTemplates;
import com.querydsl.sql.RelationalPathBase;
import com.querydsl.sql.SQLTemplates;
import com.querydsl.sql.mysql.MySQLQueryFactory;
import com.querydsl.sql.types.Type;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * ExampleTest class with QueryDSL configuration and JSON handling for DataSource properties.
 */
@SpringBootConfiguration
@Import(ExampleTest.QueryDSLConfiguration.class)
@SpringBootTest
@EnableAutoConfiguration
class ExampleTest {

    /**
     * Interface representing a JSON entity.
     */
    interface JsonEntity {
    }

    /**
     * JSON type handler using ObjectMapper to convert JSON strings to specified type objects.
     */
    static class JSONType implements Type<JsonEntity> {
        private final ObjectMapper objectMapper;

        public JSONType() {
            objectMapper = new ObjectMapper();
            objectMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
            objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
            objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        }

        @Override
        public int[] getSQLTypes() {
            return new int[]{Types.BLOB, Types.VARBINARY, Types.VARCHAR, Types.BINARY};
        }

        @Override
        public Class<JsonEntity> getReturnedClass() {
            return JsonEntity.class;
        }

        @Override
        public String getLiteral(JsonEntity value) {
            if (value == null) {
                return null;
            }
            try {
                return objectMapper.writeValueAsString(value);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public JsonEntity getValue(ResultSet rs, int startIndex) throws SQLException {
            try {
                return objectMapper.readValue(rs.getBytes(startIndex), JsonEntity.class);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public JsonEntity getValue(ResultSet rs, int startIndex, Class<JsonEntity> clazz) throws SQLException {
            try {
                return objectMapper.readValue(rs.getBytes(startIndex), clazz);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void setValue(PreparedStatement st, int startIndex, JsonEntity value) throws SQLException {
            st.setObject(startIndex, getLiteral(value));
        }
    }

    /**
     * DataSourceProperties class representing the properties of a data source.
     */
    public static class DataSourceProperties implements JsonEntity {
        private String hostname;
        private int port;
        private String schema;
        private String username;
        private String password;
        private Map<String, Object> connectionProperties;

        public String getHostname() {
            return hostname;
        }

        public void setHostname(String hostname) {
            this.hostname = hostname;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public String getSchema() {
            return schema;
        }

        public void setSchema(String schema) {
            this.schema = schema;
        }

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public Map<String, Object> getConnectionProperties() {
            return connectionProperties;
        }

        public void setConnectionProperties(Map<String, Object> connectionProperties) {
            this.connectionProperties = connectionProperties;
        }
    }

    /**
     * DataSourceEntity class representing a data source entity.
     *
     * <pre>
     *     CREATE TABLE `data_source`  (
     *      `id` int NOT NULL AUTO_INCREMENT,
     *      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
     *      `properties` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
     *       PRIMARY KEY (`id`) USING BTREE
     *     ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
     *
     *     INSERT INTO `data_source` VALUES (1, 'mysql-localhost', '{\"hostname\": \"localhost\", \"port\": 3306, \"schema\": \"test\", \"username\": \"root\", \"password\": \"root\", \"connectionProperties\": {\"useSSL\": true, \"useCursorFetch\": true}}');
     * </pre>
     */
    public static class DataSourceEntity {
        private Integer id;
        private String name;
        private DataSourceProperties properties;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public DataSourceProperties getProperties() {
            return properties;
        }

        public void setProperties(DataSourceProperties properties) {
            this.properties = properties;
        }
    }

    /**
     * QDataSourceEntity class representing the QueryDSL path for the DataSourceEntity.
     */
    public static class QDataSourceEntity extends RelationalPathBase<DataSourceEntity> {
        public QDataSourceEntity() {
            this(DataSourceEntity.class, "ds", null, "data_source");
        }

        public QDataSourceEntity(Class<? extends DataSourceEntity> type, String variable, String schema, String table) {
            super(type, variable, schema, table);
        }

        @Override
        public List<Path<?>> getColumns() {
            return Arrays.asList(
                    createNumber("id", Integer.class),
                    createString("name"),
                    createSimple("properties", DataSourceProperties.class)
            );
        }
    }

    /**
     * QueryDSL configuration class.
     */
    static class QueryDSLConfiguration {
        /**
         * SQLTemplates implementation for MySQL.
         */
        @Bean
        public SQLTemplates sqlTemplates() {
            return new MySQLTemplates();
        }

        /**
         * MySQLQueryFactory - main entry point to build and execute SQL queries.
         */
        @Bean
        public MySQLQueryFactory queryFactory(DataSource dataSource) {
            Configuration configuration = new Configuration(new MySQLTemplates());
            configuration.register(new JSONType());
            return new MySQLQueryFactory(configuration, () -> DataSourceUtils.getConnection(dataSource));
        }
    }

    @Autowired
    private MySQLQueryFactory queryFactory;

    @Test
    public void test() {
        QDataSourceEntity qDataSource = new QDataSourceEntity();
        List<DataSourceEntity> list = queryFactory.selectFrom(qDataSource).fetch();
        DataSourceEntity dataSourceEntity = list.get(0);

        assertEquals("localhost", dataSourceEntity.getProperties().getHostname());
    }
}
velo commented 3 months ago

Hi @quicklyfast, I tried to incorporate some more of your example into querydsl code base

Please let me know what you think