前言
前阵子和敌人聊天,他说他们我的项目有个需要,要实现主键主动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键主动递增不就得了。他的答复是他们我的项目目前的 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…