前言
前陣子和朋友聊天,他說(shuō)他們項(xiàng)目有個(gè)需求抵皱,要實(shí)現(xiàn)主鍵自動(dòng)生成,不想每次新增的時(shí)候辩蛋,都手動(dòng)設(shè)置主鍵呻畸。于是我就問(wèn)他,那你們數(shù)據(jù)庫(kù)表設(shè)置主鍵自動(dòng)遞增不就得了悼院。他的回答是他們項(xiàng)目目前的id都是采用雪花算法來(lái)生成伤为,因此為了項(xiàng)目穩(wěn)定性,不會(huì)切換id的生成方式。
朋友問(wèn)我有沒(méi)有什么實(shí)現(xiàn)思路绞愚,他們公司的orm框架是mybatis叙甸,我就建議他說(shuō),不然讓你老大把mybatis切換成mybatis-plus位衩。mybatis-plus就支持注解式的id自動(dòng)生成裆蒸,而且mybatis-plus只是對(duì)mybatis進(jìn)行增強(qiáng)不做改變。朋友還是那句話糖驴,說(shuō)為了項(xiàng)目穩(wěn)定齿穗,之前項(xiàng)目組沒(méi)有使用mybatis-plus的經(jīng)驗(yàn)唯咬,貿(mào)然切換不知道會(huì)不會(huì)有什么坑讶凉。后面沒(méi)招了问欠,我就跟他說(shuō)不然你用mybatis的攔截器實(shí)現(xiàn)一個(gè)吧。于是又有一篇吹水的創(chuàng)作題材出現(xiàn)跷睦。
前置知識(shí)
在介紹如何通過(guò)mybatis攔截器實(shí)現(xiàn)主鍵自動(dòng)生成之前筷弦,我們先來(lái)梳理一些知識(shí)點(diǎn)
1肋演、mybatis攔截器的作用
mybatis攔截器設(shè)計(jì)的初衷就是為了供用戶在某些時(shí)候可以實(shí)現(xiàn)自己的邏輯而不必去動(dòng)mybatis固有的邏輯
2抑诸、Interceptor攔截器
每個(gè)自定義攔截器都要實(shí)現(xiàn)
org.apache.ibatis.plugin.Interceptor
這個(gè)接口,并且自定義攔截器類(lèi)上添加@Intercepts注解
3爹殊、攔截器能攔截哪些類(lèi)型
Executor:攔截執(zhí)行器的方法蜕乡。
ParameterHandler:攔截參數(shù)的處理。
ResultHandler:攔截結(jié)果集的處理梗夸。
StatementHandler:攔截Sql語(yǔ)法構(gòu)建的處理层玲。
4、攔截的順序
a反症、不同類(lèi)型攔截器的執(zhí)行順序
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
b辛块、多個(gè)攔截器攔截同種類(lèi)型同一個(gè)目標(biāo)方法,執(zhí)行順序是后配置的攔截器先執(zhí)行
比如在mybatis配置如下
<plugins>
<plugin interceptor="com.lybgeek.InterceptorA" />
<plugin interceptor="com.lybgeek.InterceptorB" />
</plugins>
則InterceptorB先執(zhí)行铅碍。
如果是和spring做了集成润绵,先注入spring ioc容器的攔截器,則后執(zhí)行胞谈。比如有個(gè)mybatisConfig尘盼,里面有如下攔截器bean配置
@Bean
public InterceptorA interceptorA(){
return new InterceptorA();
}
@Bean
public InterceptorB interceptorB(){
return new InterceptorB();
}
則InterceptorB先執(zhí)行。當(dāng)然如果你是直接用@Component注解這形式烦绳,則可以配合@Order注解來(lái)控制加載順序
5卿捎、攔截器注解介紹
@Intercepts:標(biāo)識(shí)該類(lèi)是一個(gè)攔截器
@Signature:指明自定義攔截器需要攔截哪一個(gè)類(lèi)型,哪一個(gè)方法径密。
@Signature注解屬性中的type表示對(duì)應(yīng)可以攔截四種類(lèi)型(Executor午阵、ParameterHandler、ResultHandler享扔、StatementHandler)中的一種底桂;method表示對(duì)應(yīng)類(lèi)型(Executor括细、ParameterHandler、ResultHandler戚啥、StatementHandler)中的哪類(lèi)方法奋单;args表示對(duì)應(yīng)method中的參數(shù)類(lèi)型
6、攔截器方法介紹
a猫十、 intercept方法
public Object intercept(Invocation invocation) throws Throwable
這個(gè)方法就是我們來(lái)執(zhí)行我們自己想實(shí)現(xiàn)的業(yè)務(wù)邏輯览濒,比如我們的主鍵自動(dòng)生成邏輯就是在這邊實(shí)現(xiàn)。
Invocation這個(gè)類(lèi)中的成員屬性target就是@Signature中的type拖云;method就是@Signature中的method贷笛;args就是@Signature中的args參數(shù)類(lèi)型的具體實(shí)例對(duì)象
b、 plugin方法
public Object plugin(Object target)
這個(gè)是用返回代理對(duì)象或者是原生代理對(duì)象宙项,如果你要返回代理對(duì)象乏苦,則返回值可以設(shè)置為
Plugin.wrap(target, this);
this為攔截器
如果返回是代理對(duì)象,則會(huì)執(zhí)行攔截器的業(yè)務(wù)邏輯尤筐,如果直接返回target汇荐,就是沒(méi)有攔截器的業(yè)務(wù)邏輯。說(shuō)白了就是告訴mybatis是不是要進(jìn)行攔截盆繁,如果要攔截掀淘,就生成代理對(duì)象,不攔截是生成原生對(duì)象
c油昂、 setProperties方法
public void setProperties(Properties properties)
用于在Mybatis配置文件中指定一些屬性
主鍵自動(dòng)生成思路
1革娄、定義一個(gè)攔截器
主要攔截
`Executor#update(MappedStatement ms, Object parameter)`}
這個(gè)方法。mybatis的insert冕碟、update拦惋、delete都是通過(guò)這個(gè)方法,因此我們通過(guò)攔截這個(gè)這方法安寺,來(lái)實(shí)現(xiàn)主鍵自動(dòng)生成厕妖。其代碼塊如下
@Intercepts(value={@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}
2、判斷sql操作類(lèi)型
Executor 提供的方法中我衬,update 包含了 新增叹放,修改和刪除類(lèi)型,無(wú)法直接區(qū)分挠羔,需要借助 MappedStatement 類(lèi)的屬性 SqlCommandType 來(lái)進(jìn)行判斷,該類(lèi)包含了所有的操作類(lèi)型
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}
當(dāng)SqlCommandType類(lèi)型是insert我們才進(jìn)行主鍵自增操作
3俱恶、填充主鍵值
3.1、編寫(xiě)自動(dòng)生成id注解
Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
/**
* 主鍵名
* @return
*/
String primaryKey();
/**
* 支持的主鍵算法類(lèi)型
* @return
*/
IdType type() default IdType.SNOWFLAKE;
enum IdType{
SNOWFLAKE
}
}
3.2合是、 雪花算法實(shí)現(xiàn)
我們可以直接拿hutool這個(gè)工具包提供的idUtil來(lái)直接實(shí)現(xiàn)算法了罪。
引入
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();
3.3聪全、填充主鍵值
其實(shí)現(xiàn)核心是利用反射。其核心代碼片段如下
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);
}
}
如果項(xiàng)目中的mapper.xml已經(jīng)的insert語(yǔ)句已經(jīng)含有id难礼,比如
insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
則只需到填充id值這一步娃圆。攔截器的任務(wù)就完成。如果mapper.xml的insert不含id蛾茉,形如
insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})
則還需重寫(xiě)insert語(yǔ)句以及新增id參數(shù)
4、重寫(xiě)insert語(yǔ)句以及新增id參數(shù)(可選)
4.1 重寫(xiě)insert語(yǔ)句
方法一:
從 MappedStatement 對(duì)象中獲取 SqlSource 對(duì)象悦屏,再?gòu)膹?SqlSource 對(duì)象中獲取獲取 BoundSql 對(duì)象键思,通過(guò) BoundSql#getSql 方法獲取原始的sql,最后在原始sql的基礎(chǔ)上追加id
方法二:
引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
通過(guò)
com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser
獲取相應(yīng)的表名稚机、需要insert的字段名获搏。然后重新拼湊出新的insert語(yǔ)句
4.2 把新的sql重置給Invocation
其核心實(shí)現(xiàn)思路是創(chuàng)建一個(gè)新的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參數(shù)
其核心是利用
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
注解墓贿。也可以通過(guò)
@Bean
public AutoIdInterceptor autoIdInterceptor(){
return new AutoIdInterceptor();
}
6、在需要實(shí)現(xiàn)自增主鍵的實(shí)體字段上加如下注解
@AutoId(primaryKey = "id")
private Long id;
測(cè)試
1队伟、對(duì)應(yīng)的測(cè)試實(shí)體以及單元測(cè)試代碼如下
@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嗜侮、當(dāng)mapper的insert語(yǔ)句中含有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>
查看控制臺(tái)sql打印語(yǔ)句
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
查看數(shù)據(jù)庫(kù)
3、當(dāng)mapper的insert語(yǔ)句中不含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>
查看控制臺(tái)sql打印語(yǔ)句
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
從控制臺(tái)我們可以看出覆醇,當(dāng)mapper.xml沒(méi)有配置id字段時(shí),則攔截器會(huì)自動(dòng)幫我們追加id字段
查看數(shù)據(jù)庫(kù)
總結(jié)
本文雖然是介紹mybatis攔截器實(shí)現(xiàn)主鍵自動(dòng)生成柴罐,但文中更多講解如何實(shí)現(xiàn)一個(gè)攔截器以及主鍵生成思路革屠,并沒(méi)把intercept實(shí)現(xiàn)主鍵方法貼出來(lái)排宰。其原因主要是主鍵自動(dòng)生成在mybatis-plus里面就有實(shí)現(xiàn),其次是有思路后党瓮,大家就可以自己實(shí)現(xiàn)了盐类。最后對(duì)具體實(shí)現(xiàn)感興趣的朋友,可以查看文末中demo鏈接
參考文檔
mybatis攔截器
mybatis插件實(shí)現(xiàn)自定義改寫(xiě)表名
mybatis攔截器枪萄,動(dòng)態(tài)修改sql語(yǔ)句
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatis-autoId