关于mybatis:springboot2结合mybatis拦截器实现主键自动生成

前言

前阵子和敌人聊天,他说他们我的项目有个需要,要实现主键主动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键主动递增不就得了。他的答复是他们我的项目目前的id都是采纳雪花算法来生成,因而为了我的项目稳定性,不会切换id的生成形式。

敌人问我有没有什么实现思路,他们公司的orm框架是mybatis,我就倡议他说,不然让你老大把mybatis切换成mybatis-plus。mybatis-plus就反对注解式的id主动生成,而且mybatis-plus只是对mybatis进行加强不做扭转。敌人还是那句话,说为了我的项目稳固,之前项目组没有应用mybatis-plus的教训,贸然切换不晓得会不会有什么坑。前面没招了,我就跟他说不然你用mybatis的拦截器实现一个吧。于是又有一篇吹水的创作题材呈现。

前置常识

在介绍如何通过mybatis拦截器实现主键主动生成之前,咱们先来梳理一些知识点

1、mybatis拦截器的作用

mybatis拦截器设计的初衷就是为了供用户在某些时候能够实现本人的逻辑而不用去动mybatis固有的逻辑

2、Interceptor拦截器

每个自定义拦截器都要实现

org.apache.ibatis.plugin.Interceptor

这个接口,并且自定义拦截器类上增加@Intercepts注解

3、拦截器能拦挡哪些类型

  • Executor:拦挡执行器的办法。
  • ParameterHandler:拦挡参数的解决。
  • ResultHandler:拦挡后果集的解决。
  • StatementHandler:拦挡Sql语法构建的解决。

4、拦挡的程序

a、不同类型拦截器的执行程序

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

b、多个拦截器拦挡同种类型同一个指标办法,执行程序是后配置的拦截器先执行

比方在mybatis配置如下

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

则InterceptorB先执行。

如果是和spring做了集成,先注入spring ioc容器的拦截器,则后执行。比方有个mybatisConfig,外面有如下拦截器bean配置

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

    @Bean
    public InterceptorB interceptorB(){
        return new InterceptorB();
    }

则InterceptorB先执行。当然如果你是间接用@Component注解这模式,则能够配合@Order注解来管制加载程序

5、拦截器注解介绍

@Intercepts:标识该类是一个拦截器

@Signature:指明自定义拦截器须要拦挡哪一个类型,哪一个办法。
@Signature注解属性中的type示意对应能够拦挡四种类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的一种;method示意对应类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的哪类办法;args示意对应method中的参数类型

6、拦截器办法介绍

a、 intercept办法

public Object intercept(Invocation invocation) throws Throwable

这个办法就是咱们来执行咱们本人想实现的业务逻辑,比方咱们的主键主动生成逻辑就是在这边实现。

Invocation这个类中的成员属性target就是@Signature中的type;method就是@Signature中的method;args就是@Signature中的args参数类型的具体实例对象

b、 plugin办法

public Object plugin(Object target)

这个是用返回代理对象或者是原生代理对象,如果你要返回代理对象,则返回值能够设置为

Plugin.wrap(target, this);
this为拦截器

如果返回是代理对象,则会执行拦截器的业务逻辑,如果间接返回target,就是没有拦截器的业务逻辑。说白了就是通知mybatis是不是要进行拦挡,如果要拦挡,就生成代理对象,不拦挡是生成原生对象

c、 setProperties办法

public void setProperties(Properties properties)

用于在Mybatis配置文件中指定一些属性

主键主动生成思路

1、定义一个拦截器

次要拦挡

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

这个办法。mybatis的insert、update、delete都是通过这个办法,因而咱们通过拦挡这个这办法,来实现主键主动生成。其代码块如下

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

2、判断sql操作类型

Executor 提供的办法中,update 蕴含了 新增,批改和删除类型,无奈间接辨别,须要借助 MappedStatement 类的属性 SqlCommandType 来进行判断,该类蕴含了所有的操作类型

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

当SqlCommandType类型是insert咱们才进行主键自增操作

3、填充主键值

3.1、编写主动生成id注解

Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /**
     * 主键名
     * @return
     */
    String primaryKey();

    /**
     * 反对的主键算法类型
     * @return
     */
    IdType type() default IdType.SNOWFLAKE;

    enum IdType{
        SNOWFLAKE
    }
}

3.2、 雪花算法实现

咱们能够间接拿hutool这个工具包提供的idUtil来间接实现算法。

引入

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();

3.3、填充主键值

其实现外围是利用反射。其外围代码片段如下

   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;
                        }
                    }
                });
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{
        long value = snowflake.nextId();
        field.set(entity,value);
    }
}

如果我的项目中的mapper.xml曾经的insert语句曾经含有id,比方

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

则只需到填充id值这一步。拦截器的工作就实现。如果mapper.xml的insert不含id,形如

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

则还需重写insert语句以及新增id参数

4、重写insert语句以及新增id参数(可选)

4.1 重写insert语句

办法一:
从 MappedStatement 对象中获取 SqlSource 对象,再从从 SqlSource 对象中获取获取 BoundSql 对象,通过 BoundSql#getSql 办法获取原始的sql,最初在原始sql的根底上追加id

办法二:

引入

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

通过

com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser

获取相应的表名、须要insert的字段名。而后从新拼凑出新的insert语句

4.2 把新的sql重置给Invocation

其外围实现思路是创立一个新的MappedStatement,新的MappedStatement绑定新sql,再把新的MappedStatement赋值给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;

    }

4.3 新增id参数

其外围是利用

org.apache.ibatis.mapping.ParameterMapping

外围代码片段如下

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

5、将mybatis拦截器注入到spring容器

能够间接在拦截器上加

@org.springframework.stereotype.Component

注解。也能够通过

 @Bean
    public AutoIdInterceptor autoIdInterceptor(){
        return new AutoIdInterceptor();
    }

6、在须要实现自增主键的实体字段上加如下注解

@AutoId(primaryKey = "id")
    private Long id;

测试

1、对应的测试实体以及单元测试代码如下

@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;
}
@Autowired
    private TestService testService;

    @Test
    public void testAdd(){
        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(){
        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);
    }

2、当mapper的insert语句中含有id,形如下

<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>

以及批量插入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>

查看控制台sql打印语句

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
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

查看数据库

3、当mapper的insert语句中不含id,形如下

<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>

以及批量插入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>

查看控制台sql打印语句

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
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

从控制台咱们能够看出,当mapper.xml没有配置id字段时,则拦截器会主动帮咱们追加id字段

查看数据库

总结

本文尽管是介绍mybatis拦截器实现主键主动生成,但文中更多解说如何实现一个拦截器以及主键生成思路,并没把intercept实现主键办法贴出来。其起因次要是主键主动生成在mybatis-plus外面就有实现,其次是有思路后,大家就能够本人实现了。最初对具体实现感兴趣的敌人,能够查看文末中demo链接

参考文档

mybatis拦截器
mybatis插件实现自定义改写表名
mybatis拦截器,动静批改sql语句

demo链接

https://github.com/lyb-geek/s…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理