This is the 17th day of my participation in the First Challenge 2022

Introduction to the

In the previous part, we completed the MyBatis part of the function of the building, has been able to write Mapper interface class, automatic execution of the relevant statements, then improve the result processing part

Final effect display

Let’s modify our Mapper

public interface PersonMapper {

    @Select("select * from person")
    List<Person> list(a);

    @Insert("insert into person (id, name) values ('1', '1')")
    void save(a);
}
Copy the code

The test code is as follows:

public class SelfMybatisTest {

    @Test
    public void test(a) {
        try(SelfSqlSession session = buildSqlSessionFactory()) { PersonMapper personMapper = session.getMapper(PersonMapper.class);  personMapper.save(); List<Person> personList = personMapper.list();for(Object person: personList) { System.out.println(person.toString()); }}}public static SelfSqlSession buildSqlSessionFactory(a) {
        String JDBC_DRIVER = "org.h2.Driver";
        String DB_URL = "jdbc:h2:file:./testDb";
        String USER = "sa";
        String PASS = "";

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(DB_URL);
        config.setUsername(USER);
        config.setPassword(PASS);
        config.setDriverClassName(JDBC_DRIVER);
        config.addDataSourceProperty("cachePrepStmts"."true");
        config.addDataSourceProperty("prepStmtCacheSize"."250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit"."2048");
        DataSource dataSource = new HikariDataSource(config);

        SelfConfiguration configuration = new SelfConfiguration(dataSource);
        configuration.addMapper(PersonMapper.class);
        return newSelfSqlSession(configuration); }}Copy the code

The output is as follows:

add sql source: mapper.mapper.PersonMapper.list
add sql source: mapper.mapper.PersonMapper.save
executor
executor
Person(id=1, name=1)
Copy the code

We successfully returned our custom Person object. The Demo already looks a little bit like this

Below are the implementation details

The Demo code

The full project is available on GitHub: github.com/lw124392545…

The corresponding Tag for this article is: V2

Thinking to comb

To convert SQL query results to Java objects, we need the following:

  • 1. Returned Java object information
  • 2. Field information of the SQL table
  • 3. Conversion of SQL field values to Java object fields
  • 4. Read SQL results and convert them to Java objects

1. Returned Java object information

We need to know the Java object information returned by the current interface method, so that we can read the SQL query results later and convert them into Java objects

Taking a page from MyBatis, we define the following class to hold TypeHandler handlers for objects returned by interface methods and SQL query result fields and Java object fields

@Builder
@Data
public class ResultMap {

    private Object returnType;
    private Map<String,TypeHandler> typeHandlerMaps;
}
Copy the code

2. Field information of the SQL table

In the previous MyBatis source code analysis, we know about the TypeHandler is based on JavaType and JdbcType, we need to know the types of each field in the database table, convenient to match the corresponding TypeHandler

When we initialize the program, we read all the tables in the database and save the corresponding jdbcType of each field

Different tables may have related fields, but of different types, so the first layer is the table name, the second layer is the field name, and finally corresponds to its jdbcType

The code is as follows:

public class SelfConfiguration {

    /** * Read all tables in the database * get the corresponding types of their fields *@throws SQLException e
     */
    private void initJdbcTypeCache(a) throws SQLException {
        try (Connection conn = dataSource.getConnection()){
            final DatabaseMetaData dbMetaData = conn.getMetaData();
            ResultSet tableNameRes = dbMetaData.getTables(conn.getCatalog(),null.null.new String[] { "TABLE" });
            final List<String> tableNames = new ArrayList<>(tableNameRes.getFetchSize());
            while (tableNameRes.next()) {
                tableNames.add(tableNameRes.getString("TABLE_NAME"));
            }

            for (String tableName : tableNames) {
                try {
                    String sql = "select * from " + tableName;
                    PreparedStatement ps = conn.prepareStatement(sql);
                    ResultSet rs = ps.executeQuery();
                    ResultSetMetaData meta = rs.getMetaData();
                    int columnCount = meta.getColumnCount();
                    Map<String, Integer> jdbcTypeMap = new HashMap<>(columnCount);
                    for (int i = 1; i < columnCount + 1; i++) {
                        jdbcTypeMap.put(meta.getColumnName(i).toLowerCase(), meta.getColumnType(i));
                    }
                    jdbcTypeCache.put(tableName.toLowerCase(), jdbcTypeMap);
                } catch (Exception ignored) {
                }
            }
        }
    }
}
Copy the code

3. Conversion of SQL field values to Java object fields

Next we define TypeHandler for JavaType and jdbcType conversions

To simplify things, we have built-in String and Long handlers that are registered at initialization (no argument conversion handlers are involved, so we’ll define jdbcType to JavaType handlers for now)

The TypeHandler code looks like this:

public interface TypeHandler {

    Object getResult(ResultSet res, String cluName) throws SQLException;
}

public class StringTypeHandler implements TypeHandler {

    private static final StringTypeHandler instance = new StringTypeHandler();

    public static StringTypeHandler getInstance(a) {
        return instance;
    }

    @Override
    public Object getResult(ResultSet res, String cluName) throws SQLException {
        returnres.getString(cluName); }}public class LongTypeHandler implements TypeHandler {

    private static LongTypeHandler instance = new LongTypeHandler();

    public static LongTypeHandler getInstance(a) {
        return instance;
    }

    @Override
    public Object getResult(ResultSet res, String cluName) throws SQLException {
        returnres.getLong(cluName); }}Copy the code

Default initial registration:

public class SelfConfiguration {

    private void initTypeHandlers(a) {
        final Map<Integer, TypeHandler> varchar = new HashMap<>();
        varchar.put(JDBCType.VARCHAR.getVendorTypeNumber(), StringTypeHandler.getInstance());
        typeHandlerMap.put(String.class, varchar);

        final Map<Integer, TypeHandler> intType = newHashMap<>(); intType.put(JDBCType.INTEGER.getVendorTypeNumber(), LongTypeHandler.getInstance()); typeHandlerMap.put(Long.class, intType); }}Copy the code

The next important step is to construct the return result of the interface function method. The details are as follows, and the key points are annotated:

public class SelfConfiguration {

    private final DataSource dataSource;
    private final Map<String, SqlSource> sqlCache = new HashMap<>();
    private final Map<String, ResultMap> resultMapCache = new HashMap<>();
    private final Map<String, Map<String, Integer>> jdbcTypeCache = new HashMap<>();
    private finalMap<Class<? >, Map<Integer, TypeHandler>> typeHandlerMap =new HashMap<>();

        /** * Mapper adds * method path as unique ID * holds the SQL type and method of the interface method * holds the interface method return type *@param mapperClass mapper
     */
    public void addMapper(Class
        mapperClass) {
        final String classPath = mapperClass.getPackageName();
        final String className = mapperClass.getName();
        for (Method method: mapperClass.getMethods()) {
            final String id = StringUtils.joinWith("." ,classPath, className, method.getName());
            for (Annotation annotation: method.getAnnotations()) {
                if (annotation instanceof Select) {
                    addSqlSource(id, ((Select) annotation).value(), SqlType.SELECT);
                    continue;
                }
                if (annotation instanceofInsert) { addSqlSource(id, ((Insert) annotation).value(), SqlType.INSERT); }}// Build interface function method return value processingaddResultMap(id, method); }}/** * Build interface function method return value processing *@paramId Id of the interface function *@paramMethod Interface function method */
    private void addResultMap(String id, Method method) {
        // Return null
        if (method.getReturnType().getName().equals("void")) {
            return;
        }

        // Get the return object type
        // Special processing is needed to get the objects in the List
        Type type = method.getGenericReturnType();
        Type returnType;
        if (type instanceof ParameterizedType) {
            returnType = ((ParameterizedType) type).getActualTypeArguments()[0];
        } else {
            returnType = method.getReturnType();
        }
        // The interface method id is the key, and the value is the TypeHandler mapping of the object type returned by the interface method and the corresponding processing for each field within itresultMapCache.put(id, ResultMap.builder() .returnType(returnType) .typeHandlerMaps(buildTypeHandlerMaps((Class<? >) returnType)) .build()); }/** * Each field of the entity class is built to correspond to the TypeHandler map processed *@paramReturnType The interface function returns the object type *@returnTypeHandler map * /
    private Map<String, TypeHandler> buildTypeHandlerMaps(Class
        returnType) {
        // by default, the lower case of the class name is the corresponding database TableName. You can also use an annotation like @tablename
        final String tableName = StringUtils.substringAfterLast(returnType.getTypeName(), ".").toLowerCase();
        final Map<String, TypeHandler> typeHandler = new HashMap<>(returnType.getDeclaredFields().length);
        for (Field field : returnType.getDeclaredFields()) {
            final String javaType = field.getType().getName();
            final String name = field.getName();
            final Integer jdbcType = jdbcTypeCache.get(tableName).get(name);
            // Get the corresponding TypeHandler based on JavaType and jdbcType
            typeHandler.put(javaType, typeHandlerMap.get(field.getType()).get(jdbcType));
        }
        returntypeHandler; }}Copy the code

4. Read SQL results and convert them to Java objects

Next comes the processing of SQL query results, based on a ResultMap built during initialization for each return type

  • Generate an object instance based on the returned object type in the ResultMap
  • According to the TypeHandler mapping in ResultMap, the Corresponding TypeHandler of each field is obtained, and the processing result is obtained
  • Reflection calls the Set method of an object

The code is as follows:

public class ResultHandler {

    public List<Object> parse(String id, ResultSet res, SelfConfiguration config) throws SQLException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        if (res == null) {
            return null;
        }

        // Get the ResultMap of the national team when initialization according to the interface function Id
        ResultMap resultMap = config.getResultType(id);

        final List<Object> list = new ArrayList<>(res.getFetchSize());
        while (res.next()) {
            // Generate an instance based on the interface function return typeClass<? > returnType = (Class<? >) resultMap.getReturnType(); Object val = returnType.getDeclaredConstructor().newInstance();for (Field field: returnType.getDeclaredFields()) {
                final String name = field.getName();
                // According to the field type of the returned object, get the corresponding TypeHandler, call TypeHandler to get the result
                TypeHandler typeHandler = resultMap.getTypeHandlerMaps().get(field.getType().getName());
                Object value = typeHandler.getResult(res, name);
                // Call the Set method of the object
                String methodEnd = name.substring(0.1).toUpperCase() + name.substring(1);
                Method setMethod = val.getClass().getDeclaredMethod("set" + methodEnd, field.getType());
                setMethod.invoke(val, value);
            }
            list.add(val);
        }
        returnlist; }}Copy the code

conclusion

This article has improved the automatic processing and conversion part of the query result set in Demo. After completion, the core functions have been completed and basically achieved the goal