1. Introduction

Today we continue to build our Kono Spring Boot scaffolding, the last article to the most popular ORM framework Mybatis also integrated into it. But a lot of times we want to have some generic Mapper out of the box to simplify our development. I tried to implement one myself, and I’m going to share the idea. It was written last night, carefully used for actual production development, but can be used for reference.

Gitee: gitee.com/felord/kono day03 branch

Making: github.com/NotFound403… Day03 branch

2. Source of ideas

I’ve been reading some stuff about Spring Data JDBC and found it quite good. CrudRepository is magic, as long as the ORM interface inherits it, it is automatically added to Spring IoC, and it also has some basic database operation interfaces. I was wondering if I could combine it with Mybatis.

Spring Data JDBC itself supports Mybatis. However, after I tried to integrate them, I found that there are a lot of things to do and many conventions to follow, such as the parameter context of MybatisContext and the interface name prefix, which are relatively strict conventions, and the cost of learning to use is relatively high, which is not as good as using Spring Data JDBC alone. But I still wanted universal CRUD functionality, so I started experimenting with a simple one myself.

3. Try something

There were several ideas that came to mind at first but none of them worked out. Here is also to share, sometimes failure is also very worth learning.

3.1 Mybatis plugin

The plug-in function of Mybatis is used to develop the plug-in, but it is not feasible after a long time of research. The biggest problem is the life cycle of Mapper.

The Mapper is registered in the configuration at project startup, and the corresponding SQL is also registered in the MappedStatement object. When the Mapper method is executed, the SQL is retrieved and executed by an agent that loads the corresponding MappedStatement based on the Namespace.

The plug-in life cycle starts when the MappedStatement has been registered and does not connect at all.

3.2 Code Generator

This is completely feasible, but the cost of making wheels is higher and more mature. In the actual production and development, we will find one. It costs more time and energy to make an artificial wheel, and it is unnecessary.

3.3 Simulating MappedStatement registration

Finally, find a suitable entry point to register the MappedStatement corresponding to the universal Mapper. I’ll talk more about how I did this next.

4. Spring registration mechanism for Mapper

In the early days of Spring Boot, most Mapper registrations were done this way.

  <bean id="baseMapper" class="org.mybatis.spring.mapper.MapperFactoryBean" abstract="true" lazy-init="true">
     <property name="sqlSessionFactory" ref="sqlSessionFactory" />
   </bean>
   <bean id="oneMapper" parent="baseMapper">
     <property name="mapperInterface" value="my.package.MyMapperInterface" />
   </bean>
   <bean id="anotherMapper" parent="baseMapper">
     <property name="mapperInterface" value="my.package.MyAnotherMapperInterface" />
  </bean>
Copy the code

Each Mybatis Mapper is initialized and injected into the Spring IoC container via MapperFactoryBean. So this is a place where injection of a generic Mapper is possible and less invasive. So how does it work? I found it in the familiar @mapperscan. The following is an excerpt from its source code:

/**
 * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
 *
 * @return the class of {@code MapperFactoryBean}
 */
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
Copy the code

That is, usually @Mapperscan will batch initialize and inject Spring IoC all Mapper in a particular package using MapperFactoryBean.

5. Implement universal Mapper

Now that you understand Spring’s Mapper registration mechanism, you’re ready to implement a universal Mapper.

5.1 General Mapper Interfaces

CrudMapper

contains four basic single-table operations. CrudMapper

contains four basic single-table operations.
,>
,>

/** * All Mapper interfaces inherit {@code CrudMapper<T, PK>}.
 *
 * @param<T> Entity class generic *@param<PK> Primary key generic *@author felord.cn
 * @since 14 :00
 */
public interface CrudMapper<T.PK> {

    int insert(T entity);

    int updateById(T entity);

    int deleteById(PK id);

    T findById(PK id);
}
Copy the code

The rest of the logic revolves around this interface. When the concrete Mapper inherits this interface, the entity class generic T and primary key generic PK are identified. We need to take the specific type of T and encapsulate its member properties into SQL and customize the MappedStatement.

5.2 Metadata parsing and encapsulation of Mapper

To simplify the code, entity classes make some common conventions:

  • The underline style of the entity class name is the corresponding table name, for exampleUserInfoThe database table name of theuser_info.
  • The underline style of the entity class attribute is the field name of the corresponding database table. In addition, all attributes in the entity have corresponding database fields, which can be ignored.
  • If the corresponding SQL exists for mapper. XML, this configuration is ignored.

Since the primary key attribute must be explicitly identified to obtain it, a primary key tag annotation is declared:

/**
 * Demarcates an identifier.
 *
 * @author felord.cn
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface PrimaryKey {
}
Copy the code

Then we declare a database entity like this:

/ * * *@author felord.cn
 * @since15:43 * * /
@Data
public class UserInfo implements Serializable {

    private static final long serialVersionUID = -8938650956516110149L;
    @PrimaryKey
    private Long userId;
    private String name;
    private Integer age;
}
Copy the code

Then you can write a working Mapper like this.

public interface UserInfoMapper extends CrudMapper<UserInfo.String> {}
Copy the code

The next step is to encapsulate a utility class CrudMapperProvider that parses this interface. What it does is parse the UserInfoMapper Mapper and encapsulate the MappedStatement. To make it easier to understand, I illustrated the process of parsing a Mapper with examples.

public CrudMapperProvider(Class
       > mapperInterface) {
    // Get the specific Mapper interface such as UserInfoMapper
    this.mapperInterface = mapperInterface;
    Type[] genericInterfaces = mapperInterface.getGenericInterfaces();
    CrudMapper
      ,string>
    Type mapperGenericInterface = genericInterfaces[0];
    // Parameterize the type
    ParameterizedType genericType = (ParameterizedType) mapperGenericInterface;

      // The purpose of the parameterized type is to understand the precipitation [UserInfo,String]
    Type[] actualTypeArguments = genericType.getActualTypeArguments();
    // Get the entity type UserInfo
    this.entityType = (Class<? >) actualTypeArguments[0];
    // Get the primary key type String
    this.primaryKeyType = (Class<? >) actualTypeArguments[1];
    // Fetching all entity-class attributes was intended to be introspective
    Field[] declaredFields = this.entityType.getDeclaredFields();

    // Resolve the primary key
    this.identifer = Stream.of(declaredFields)
            .filter(field -> field.isAnnotationPresent(PrimaryKey.class))
            .findAny()
            .map(Field::getName)
            .orElseThrow(() -> new IllegalArgumentException(String.format("no @PrimaryKey found in %s".this.entityType.getName())));

    // Parse the attribute name and encapsulate it as an underscore field to exclude static attributes. Otherwise, you can declare an ignore annotation to ignore the field if necessary
    this.columnFields = Stream.of(declaredFields) .filter(field -> ! Modifier.isStatic(field.getModifiers())) .collect(Collectors.toList());// Parse the table name
    this.table = camelCaseToMapUnderscore(entityType.getSimpleName()).replaceFirst("_"."");
}
Copy the code

Once you have that metadata, you generate four types of SQL. The SQL we expect, for example UserInfoMapper, looks like this:

#  findById
SELECT user_id, name, age FROM user_info WHERE (user_id = #{userId})
#  insert
INSERT INTO user_info (user_id, name, age) VALUES (#{userId}, #{name}, #{age})
#  deleteById 
DELETE FROM user_info WHERE (user_id = #{userId})
#  updateById
UPDATE user_info SET  name = #{name}, age = #{age} WHERE (user_id = #{userId})
Copy the code

Mybatis provides a good SQL utility class to generate these SQL:

 String findSQL = new SQL()
                .SELECT(COLUMNS)
                .FROM(table)
                .WHERE(CONDITION)
                .toString();

String insertSQL = new SQL()
                .INSERT_INTO(table)
                .INTO_COLUMNS(COLUMNS)
                .INTO_VALUES(VALUES)
                .toString();
                
String deleteSQL = new SQL()
                .DELETE_FROM(table)
                .WHERE(CONDITION).toString(); 
                
String updateSQL = new SQL().UPDATE(table)
                .SET(SETS)
                .WHERE(CONDITION).toString();                
Copy the code

We just need to use the metadata obtained by reflection to achieve dynamic creation of SQL. Take the insert method as an example:

/**
 * Insert.
 *
 * @param configuration the configuration
 */
private void insert(Configuration configuration) {
    String insertId = mapperInterface.getName().concat(".").concat("insert");
     // In XML configuration, already registered skip XML has the highest priority
    if (existStatement(configuration,insertId)){
        return;
    }
    // Generate a list of database fields
    String[] COLUMNS = columnFields.stream()
            .map(Field::getName)
            .map(CrudMapperProvider::camelCaseToMapUnderscore)
            .toArray(String[]::new);
    // The corresponding value is wrapped in #{}
    String[] VALUES = columnFields.stream()
            .map(Field::getName)
            .map(name -> String.format("#{%s}", name))
            .toArray(String[]::new);

    String insertSQL = new SQL()
            .INSERT_INTO(table)
            .INTO_COLUMNS(COLUMNS)
            .INTO_VALUES(VALUES)
            .toString();

    Map<String, Object> additionalParameters = new HashMap<>();
    / / register
    doAddMappedStatement(configuration, insertId, insertSQL, SqlCommandType.INSERT, entityType, additionalParameters);
}
Copy the code

Another important thing here is that every MappedStatement has a globally unique identifier. Mybatis defaults to punctuation for fully qualified names of Mapper. The corresponding method name on the concatenation. For example, cn. Felord. Kono. MapperClientUserRoleMapper. FindById. It’s then time to define your own MapperFactoryBean.

5.3 Customizing MapperFactoryBean

A good starting point is to register MappedStatement after Mapper is registered. We can inherit MapperFactoryBean and override its checkDaoConfig method to register MappedStatement using CrudMapperProvider.

    @Override
    protected void checkDaoConfig(a) {
        notNull(super.getSqlSessionTemplate(), "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
        Class<T> mapperInterface = super.getMapperInterface();
        notNull(mapperInterface, "Property 'mapperInterface' is required");

        Configuration configuration = getSqlSession().getConfiguration();

        if (isAddToConfig()) {
            try {
                // Check whether Mapper is registered
                if(! configuration.hasMapper(mapperInterface)) { configuration.addMapper(mapperInterface); }// Only if you inherit CrudMapper
                if (CrudMapper.class.isAssignableFrom(mapperInterface)) {
                    // An opportunity to register an SQL map
                    CrudMapperProvider crudMapperProvider = new CrudMapperProvider(mapperInterface);
                    / / register MappedStatementcrudMapperProvider.addMappedStatements(configuration); }}catch (Exception e) {
                logger.error("Error while adding the mapper '" + mapperInterface + "' to configuration.", e);
                throw new IllegalArgumentException(e);
            } finally{ ErrorContext.instance().reset(); }}}Copy the code

5.4 Enabling the Universal Mapper

Since we override the default MapperFactoryBean, we explicitly declare the custom MybatisMapperFactoryBean enabled, as follows:

@MapperScan(basePackages = {"cn.felord.kono.mapper"},factoryBean = MybatisMapperFactoryBean.class)
Copy the code

Then a universal Mapper feature is implemented.

5.5 Project Location

This is just a small attempt of my own, I have taken out this function separately, interested in their own reference research.

  • Making: github.com/NotFound403…
  • Gitee: gitee.com/felord/myba…

6. Summary

The key to success is to control the life cycle of some concepts in Mybatis. In fact, most frameworks follow this idea when they need to be modified: figure out the process and find an appropriate entry point to embed custom logic. This DEMO will not incorporate the main branch, as this is just a trial run and not yet practical, you can choose other well-known frameworks to do this. Pay attention and support: Share more about what’s going on in development.

Follow our public id: Felordcn for more information

Personal blog: https://felord.cn