preface

A few days ago, I had a chat with my friend. He said that their project had a requirement to realize automatic generation of primary keys, and they did not want to manually set the primary keys every time they were added. So I asked him, that your database table set the primary key automatically increase not got. His answer is that the current ID of their project is generated by snowflake algorithm, so for the stability of the project, the generation mode of ID will not be changed.

My friend asked me if I had any ideas on how to implement it. The ORM framework of their company is Mybatis, SO I suggested him to change MyBatis into MyBatis – Plus. Mybatis – Plus supports annotated id generation automatically, and Mybatis – Plus only enhances MyBatis without changing it. My friend said that in order to stabilize the project, the project team didn’t have experience in using Mybatis – Plus before, so I don’t know if there will be any pit if I switch it rashly. There is no way back, I told him otherwise you use Mybatis interceptor to achieve a bar. Then there is another blowing water creation theme.

Front knowledge

Mybatis interceptor: Mybatis interceptor: MyBatis interceptor: MyBatis interceptor: MyBatis interceptor

1. The role of mybatis interceptor

Mybatis interceptor is designed to allow users to implement their own logic at some point without having to deal with myBatis’ inherent logic

Interceptor = Interceptor

Each custom interceptor is implemented

org.apache.ibatis.plugin.Interceptor
Copy the code

This interface, and add the @intercepts annotation to the custom interceptor class

What types can interceptors intercept

  • Executor: Method of intercepting an Executor.

  • ParameterHandler: processing interception parameters.

  • ResultHandler: Intercepts the processing of result sets.

  • StatementHandler: Intercepts the processing of Sql syntax builds.

4, the order of interception

A. Execution sequence of different types of interceptors

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
Copy the code

B. Multiple interceptors intercept the same target method of the same type, and the order of execution is the last configured interceptor

For example, mybatis is configured as follows

	<plugins>
		<plugin interceptor="com.lybgeek.InterceptorA" />
		<plugin interceptor="com.lybgeek.InterceptorB" />
	</plugins>
Copy the code

InterceptorB executes first.

If you integrate with Spring, inject the interceptor of the Spring IOC container first and execute later. For example, there is a mybatisConfig with the following interceptor bean configuration

 @Bean
    public InterceptorA interceptorA(a){
        return new InterceptorA();
    }

    @Bean
    public InterceptorB interceptorB(a){
        return new InterceptorB();
    }
Copy the code

InterceptorB executes first. Of course, if you use the @Component annotation directly, you can use the @order annotation to control the load Order

Introduction to interceptor annotations

@intercepts: Identifies this class as an interceptor

@signature: specifies which type and method the custom interceptor needs to intercept. The type in the @signature attribute indicates that one of four types (Executor, ParameterHandler, ResultHandler, StatementHandler) can be intercepted. Method specifies the method of the corresponding type (Executor, ParameterHandler, ResultHandler, StatementHandler). Args indicates the parameter type in method

6. Introduction to interceptor methods

A. Intercept method

public Object intercept(Invocation invocation) throws Throwable
Copy the code

The method is to implement our own business logic, such as our primary key auto-generation logic is implemented here.

The target attribute in the Invocation class is the type in the @Signature; Method is @signature method; Args is a concrete instance object of the args parameter type in @signature

B. Plugin method

public Object plugin(Object target)
Copy the code

This is either a return proxy object or a native proxy object. If you want to return a proxy object, the return value can be set to

Plugin.wrap(target, this);
thisFor the interceptorCopy the code

If the return is a proxy object, the interceptor’s business logic is executed, if the target is returned directly, the business logic is not interceptor. To tell mybatis whether to intercept, if to intercept, generate proxy object, do not intercept is to generate native object

C, setProperties method

public void setProperties(Properties properties)
Copy the code

Used to specify some properties in the Mybatis profile

The primary key automatically generates ideas

Define an interceptor

Mainly to intercept

 `Executor#update(MappedStatement ms, Object parameter)`} 
Copy the code

This method. Mybatis insert, update, delete are all through this method, so we intercept this method, to achieve automatic generation of primary keys. The code block is as follows

@Intercepts(value={@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}
Copy the code

2. Determine the type of SQL operation

An Executor update method contains the types of new, modified, and deleted operations. You can use the SqlCommandType attribute of the MappedStatement class to determine the types of new, modified, and deleted operations. This class contains all operation types

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

Copy the code

The primary key increment is performed when SqlCommandType is INSERT

3. Fill in primary key values

3.1. Write auto-generated ID annotations

Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /** * primary key *@return* /
    String primaryKey(a);

    /** * Supported primary key algorithm types *@return* /
    IdType type(a) default IdType.SNOWFLAKE;

    enum IdType{
        SNOWFLAKE
    }
}
Copy the code

3.2 Implementation of snowflake algorithm

We can directly take the toolkit provided by hutool idUtil to directly implement the algorithm.

The introduction of

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
Copy the code
Snowflake snowflake = IdUtil.createSnowflake(0.0);
long value = snowflake.nextId();
Copy the code

3.3. Fill in primary key values

The core of its implementation is the use of reflection. The core code snippet is shown below

   ReflectionUtils.doWithFields(entity.getClass(), field->{
                    ReflectionUtils.makeAccessible(field);
                    AutoId autoId = field.getAnnotation(AutoId.class);
                    if(! ObjectUtils.isEmpty(autoId) && (field.getType().isAssignableFrom(Long.class))){switch (autoId.type()){
                            case SNOWFLAKE:
                                SnowFlakeAutoIdProcess snowFlakeAutoIdProcess = new SnowFlakeAutoIdProcess(field);
                                snowFlakeAutoIdProcess.setPrimaryKey(autoId.primaryKey());
                                finalIdProcesses.add(snowFlakeAutoIdProcess);
                                break; }}});Copy the code
public class SnowFlakeAutoIdProcess extends BaseAutoIdProcess {

    private static Snowflake snowflake = IdUtil.createSnowflake(0.0);


    public SnowFlakeAutoIdProcess(Field field) {
        super(field);
    }

    @Override
    void setFieldValue(Object entity) throws Exception{
        longvalue = snowflake.nextId(); field.set(entity,value); }}Copy the code

If mapper.xml in your project already has an ID in its insert statement, for example

insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
Copy the code

You just need to fill in the ID value. The interceptor’s job is done. If mapper. XML insert does not contain id, it looks like

insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})
Copy the code

You also need to rewrite the INSERT statement and add the ID parameter

4, Rewrite insert statement and add id parameter (optional)

4.1 Rewriting insert statements

Method one: Obtain the BoundSql object from the MappedStatement object, obtain the BoundSql object from the SqlSource object, use the BoundSql#getSql method to obtain the original SQL, and append the ID to the original SQL

Method 2:

The introduction of

<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>${druid.version}</version>
		</dependency>
Copy the code

through

com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser
Copy the code

Get the corresponding table name, the name of the field to insert. Then reassemble the new INSERT statement

4.2 Reset the new SQL to the Invocation

The main idea is to create a new MappedStatement that binds the new SQL and assigns the new MappedStatement to the Invocation ARgs [0]

 private void resetSql2Invocation(Invocation invocation, BoundSqlHelper boundSqlHelper,Object entity) throws SQLException {
        final Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSqlHelper));
        MetaObject msObject =  MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
        msObject.setValue("sqlSource.boundSqlHelper.boundSql.sql", boundSqlHelper.getSql());

            args[0] = newStatement;

    }
Copy the code

4.3 Adding the ID Parameter

Its core is utilization

org.apache.ibatis.mapping.ParameterMapping
Copy the code

The core code snippet is shown below

  private void setPrimaryKeyParaterMapping(String primaryKey) {
           ParameterMapping parameterMapping = new ParameterMapping.Builder(boundSqlHelper.getConfiguration(),primaryKey,boundSqlHelper.getTypeHandler()).build();
           boundSqlHelper.getBoundSql().getParameterMappings().add(parameterMapping);
       }
Copy the code

Inject the MyBatis interceptor into the Spring container

You can add it directly to the interceptor

@org.springframework.stereotype.Component
Copy the code

Annotation. Also through

 @Bean
    public AutoIdInterceptor autoIdInterceptor(a){
        return new AutoIdInterceptor();
    }
Copy the code

6. Add the following comments to the entity fields where you want to implement auto-increment primary keys

@AutoId(primaryKey = "id")
	private Long id;
Copy the code

test

1. The corresponding test entity and unit test code are as follows

@Data
public class TestDO implements Serializable {
	private static final long serialVersionUID = 1L;

	@AutoId(primaryKey = "id")
	private Long id;
	private Integer type;
	private String url;
	private Date gmtCreate;
	private String menuType;
}
Copy the code
@Autowired
    private TestService testService;

    @Test
    public void testAdd(a){
        TestDO testDO = new TestDO();
        testDO.setType(1);
        testDO.setMenuType("1");
        testDO.setUrl("www.test.com");
        testDO.setGmtCreate(new Date());
        testService.save(testDO);
        testService.get(110L);
    }

    @Test
    public void testBatch(a){
        List<TestDO> testDOList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            TestDO testDO = new TestDO();
            testDO.setType(i);
            testDO.setMenuType(i+"");
            testDO.setUrl("www.test"+i+".com");
            testDO.setGmtCreate(new Date());
            testDOList.add(testDO);
        }

        testService.saveBatch(testDOList);
    }
Copy the code

2, when a mapper insert statement contains an ID, look like this

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
		insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`)
		values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
	</insert>
Copy the code

And batch insert SQL

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
		insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`)
		values
		<foreach collection="list" item="test" index="index" separator=",">
			( #{test.id},#{test.gmtCreate},#{test.type}, #{test.url},
			#{test.menuType})
		</foreach>
	</insert>
Copy the code

View the console SQL print statements

15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`) values(? ,? ,? ,? ,?) 
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 356829258376544258(Long), 1(Integer), www.test.com(String), 1(String), 2020- 09 -11 15:52:04.738(Timestamp)
15:52:04 [main] DEBUG com.nlybgeek.dao.TestDao.save - <==    Updates: 1
Copy the code
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`) values (? ,? ,? ,? ,?) , (? ,? ,? ,? ,?) , (? ,? ,? ,? ,?) 
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 356829258896637961(Long), 2020- 09 -11 15:52:04.847(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356829258896637960(Long), 2020- 09 -11 15:52:04.847(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356829258896637962(Long), 2020- 09 -11 15:52:04.847(Timestamp), 2(Integer), www.test2.com(String), 2(String)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3
Copy the code

Viewing a Database

3, if the mapper insert statement does not contain an ID, look like this

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
		insert into sys_test(`type`, `url`,`menu_type`,`gmt_create`)
		values(#{type}, #{url},#{menuType},#{gmtCreate})
	</insert>
Copy the code

And batch insert SQL

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
		insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`)
		values
		<foreach collection="list" item="test" index="index" separator=",">
			(#{test.gmtCreate},#{test.type}, #{test.url},
			#{test.menuType})
		</foreach>
	</insert>

Copy the code

View the console SQL print statements

15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`type`,`url`,`menu_type`,`gmt_create`,id) values (? ,? ,? ,? ,?) 
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 1(Integer), www.test.com(String), 1(String), 2020- 09 -11 15:59:46.741(Timestamp), 356831196144992264(Long)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - <==    Updates: 1
Copy the code
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`,id) values (? ,? ,? ,? ,?).(? ,? ,? ,? ,?).(? ,? ,? ,? ,?) 
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 2020- 09 -11 15:59:46.845(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356831196635725829(Long), 2020- 09 -11 15:59:46.845(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356831196635725828(Long), 2020- 09 -11 15:59:46.845(Timestamp), 2(Integer), www.test2.com(String), 2(String), 356831196635725830(Long)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3
Copy the code

From the console, we can see that when mapper.xml is not configured with an ID field, the interceptor will append the ID field for us automatically

Viewing a Database

conclusion

Mybatis interceptor to achieve primary key generation, but more on how to implement an interceptor and primary key generation ideas, and the intercept implementation of primary key method posted. The main reason is that the primary key is automatically generated in Mybatis – Plus, followed by ideas, we can achieve their own. Finally, if you are interested in the implementation, you can check out the demo link at the end of this article

Reference documentation

Mybatis interceptor

Mybatis plugin implements custom overwrite table names

Mybatis interceptor, dynamic modification of SQL statements

The demo link

Github.com/lyb-geek/sp…