mybatis-plus攔截器、字段填充器、類型處理器、表名替換、SqlInjector(聯(lián)合主鍵處理)
最近有個(gè)練手的小例子,大概就是配置兩個(gè)數(shù)據(jù)源,從一個(gè)數(shù)據(jù)源讀取數(shù)據(jù)寫到另一個(gè)數(shù)據(jù)源,雖然最后做了出來,但是不支持事務(wù)。。。就當(dāng)是對(duì)mybatis-plus/mybatis組件使用方式的記錄吧,本次例子使用的仍是mybatis-plus
回憶一下mybatis核心對(duì)象:
- Configuration 初始化基礎(chǔ)配置,比如MyBatis的別名等,一些重要的類型對(duì)象,如,插件,映射器,ObjectFactory和typeHandler對(duì)象,MyBatis所有的配置信息都維持在Configuration對(duì)象之中
- SqlSessionFactory SqlSession工廠
- SqlSession 作為MyBatis工作的主要頂層API,表示和數(shù)據(jù)庫交互的會(huì)話,完成必要數(shù)據(jù)庫增刪改查功能
- Executor MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心,負(fù)責(zé)SQL語句的生成和查詢緩存的維護(hù)
- StatementHandler 封裝了JDBC Statement操作,負(fù)責(zé)對(duì)JDBC statement 的操作,如設(shè)置參數(shù)、將Statement結(jié)果集轉(zhuǎn)換成List集合。
- ParameterHandler 負(fù)責(zé)對(duì)用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement 所需要的參數(shù),
- ResultSetHandler 負(fù)責(zé)將JDBC返回的ResultSet結(jié)果集對(duì)象轉(zhuǎn)換成List類型的集合;
- TypeHandler 負(fù)責(zé)java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換
- MappedStatement MappedStatement維護(hù)了一條<select|update|delete|insert>節(jié)點(diǎn)的封裝,
- SqlSource 負(fù)責(zé)根據(jù)用戶傳遞的parameterObject,動(dòng)態(tài)地生成SQL語句,將信息封裝到BoundSql對(duì)象中,并返回
- BoundSql 表示動(dòng)態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息
組件介紹
mybatis可以在執(zhí)行語句的過程中對(duì)特定對(duì)象進(jìn)行攔截調(diào)用,主要有四個(gè)
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 處理增刪改查
- ParameterHandler (getParameterObject, setParameters) 設(shè)置預(yù)編譯參數(shù)
- ResultSetHandler (handleResultSets, handleOutputParameters) 處理結(jié)果
- StatementHandler (prepare, parameterize, batch, update, query) 處理sql預(yù)編譯,設(shè)置參數(shù)
這四個(gè)是可以攔截的對(duì)象,大概的做法是實(shí)現(xiàn)mybatis攔截器的接口并在上面添加注解來確定攔截那些方法
下面是接口Interceptor所要實(shí)現(xiàn)的方法,setPropertites可以用來初始化,而plugin則包裝目標(biāo)對(duì)象供攔截器處理,基于動(dòng)態(tài)代理實(shí)現(xiàn),Plugin類是動(dòng)態(tài)代理類,對(duì)實(shí)現(xiàn)Interceptor的接口的類進(jìn)行處理,而實(shí)現(xiàn)的攔截器會(huì)被加入到攔截器鏈進(jìn)行處理
Object intercept(Invocation var1) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
plugin.warp方法

攔截器鏈:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList();
public InterceptorChain() {
}
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
this.interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(this.interceptors);
}
}
并在handler里面添加這些攔截器類,執(zhí)行pluginAll方法,返回一個(gè)經(jīng)過代理鏈處理的對(duì)象

實(shí)現(xiàn)該接口以后,要添加注解來表明攔截哪些方法,方法則是上面四個(gè)對(duì)象的擁有的方法。下面這個(gè)注解則是指定了攔截哪些對(duì)象的哪個(gè)方法,args則是被攔截方法的參數(shù)
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
比如這個(gè)例子

@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
Signature注解就對(duì)應(yīng)上面的接口、方法及其參數(shù),然后在攔截器添加一個(gè)@Intercepts,這個(gè)注解的內(nèi)容是Signature注解數(shù)組
有了攔截器,初步想法是根據(jù)方法攔截,如果select則使用讀數(shù)據(jù)源,增刪改則使用寫數(shù)據(jù)源,這個(gè)其實(shí)原理和之前寫的一篇代碼級(jí)別讀寫分離很相似,也是通過ThreadLocal存放當(dāng)前線程的數(shù)據(jù)源,然后通過攔截器來判斷用哪個(gè)數(shù)據(jù)源,交由AbstarctRoutingDataSource來根據(jù)ThreadLoacl里面的值來處理。
但是有個(gè)問題,兩個(gè)數(shù)據(jù)源轉(zhuǎn)換,表名、字段名不一定相等,比如從pgsql的一個(gè)叫user_info表里的數(shù)據(jù)轉(zhuǎn)到mysql叫user表的數(shù)據(jù),字段名都不相同


我的處理方法是查詢對(duì)象的目標(biāo)的字段名為準(zhǔn),然后給每個(gè)字段一個(gè)注解指向修改對(duì)象的數(shù)據(jù)源表字段名,如果查詢目標(biāo)表沒有插入目標(biāo)表的字段,便在select的時(shí)候默認(rèn)select null或者用代碼限定查詢的字段。這里首先先定義了三個(gè)注解,分別對(duì)應(yīng)查、改相應(yīng)的數(shù)據(jù)源、表名、字段
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranDB {
DBType from();
DBType to();
Class object();
}
public @interface TranField {
String from() default "";
String to();
String empty = "null";
}
public @interface TranTable {
String from();
String to();
}
User類
@TranTable(from = "user_info", to = "user")
public class User {
@TranField(to = "id")
@TableId
private Integer userId;
@TranField(to = "wx_nickname")
private String userAccount;
@TranField(to = "roles")
private String mobile;
@TranField(from=TranField.empty,to="create_time")
private Date createTime;
@TranField(from=TranField.empty,to="update_time")
private Date updateTime;
@TranField(from=TranField.empty,to="bonus")
private Integer bonus;
@TranField(to="wx_id")
private String[] test;
}
UserMapper
@TranDB(from = DBType.PGSQL,to=DBType.MYSQL,object=User.class)
public interface UserMapper extends BaseMapper<User> {
}
這里添加一個(gè)緩存mapper信息類,方便在攔截器中調(diào)用,其中有個(gè)成員變量是用來存儲(chǔ)mapperName對(duì)應(yīng)的TranDB注解信息,攔截器通過攔截的方法獲取mapper名稱,再通過這個(gè)mapper信息類獲取他的TranDB注解,這個(gè)注解里面有對(duì)應(yīng)的實(shí)體class,可以用來獲取字段信息注解及表名信息注解,而另一個(gè)成員變量則是用來存放待會(huì)說到的表名替換,這里面實(shí)現(xiàn)了兩個(gè)接口,一個(gè)通過spring容器加載資源的接口,另一個(gè)則是用來初始化bean的。
package com.trendy.task.transport.config;
import com.baomidou.mybatisplus.extension.parsers.ITableNameHandler;
import com.trendy.task.transport.annotations.TranDB;
import com.trendy.task.transport.handler.SelfTableNameHandler;
import com.trendy.task.transport.util.CamelHumpUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import java.util.*;
/**
* Mapper信息緩存類
*/
public class MapperAuxFeatureMap implements ResourceLoaderAware, InitializingBean {
private static ResourceLoader resourceLoader;
@Value("${tran.mapperlocation}")
public String MAPPER_LOCATION ;
public static final String TABLEPREFIX="t_";
//表名處理
public Map<String, ITableNameHandler> tableNameHandlerMap;
//mapper文件的注解
public Map<String, TranDB> mapperTranDbMap;
//通過方法獲取mapper名稱
public static String getMapperNameFromMethodName(String source){
int end = source.lastIndexOf(".") + 1;
String mapper = source.substring(0, end - 1);
mapper = mapper.substring(mapper.lastIndexOf(".") + 1);
return mapper;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
MapperAuxFeatureMap.resourceLoader=resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
Resource[] resources = resolver.getResources("classpath*:"+MAPPER_LOCATION.replace(".","/")+"/**/*.class");
mapperTranDbMap = new HashMap<>();
tableNameHandlerMap = new HashMap<>();
for (Resource r : resources) {
MetadataReader reader = metaReader.getMetadataReader(r);
String className = reader.getClassMetadata().getClassName();
Class<?> c = Class.forName(className);
if (c.isAnnotationPresent(TranDB.class)) {
String name = c.getSimpleName();
TranDB tranDB = c.getAnnotation(TranDB.class);
mapperTranDbMap.put(name, tranDB);
String value = tranDB.object().getSimpleName();
tableNameHandlerMap.put(TABLEPREFIX+ CamelHumpUtils.humpToLine(value),new SelfTableNameHandler(tranDB.object()));
}
}
}
}
替換數(shù)據(jù)源的部分代碼,對(duì)query和update(即增刪改)方法進(jìn)行攔截,改方法使用mysql數(shù)據(jù)源,查方法使用pgsql數(shù)據(jù)源
package com.trendy.task.transport.dyma;
import com.trendy.task.transport.annotations.TranDB;
import com.trendy.task.transport.config.MapperAuxFeatureMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* @author: lele
* @date: 2019/10/23 下午4:24
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class DynamicDataSourceInterceptor implements Interceptor {
private MapperAuxFeatureMap mapperAuxFeatureMap;
public DynamicDataSourceInterceptor(MapperAuxFeatureMap mapperAuxFeatureMap) {
this.mapperAuxFeatureMap = mapperAuxFeatureMap;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
//如果讀取數(shù)據(jù),使用From的庫,否則使用To庫
DBType db =null;
Object[] objects = invocation.getArgs();
MappedStatement statement = (MappedStatement) objects[0];
String mapper = MapperAuxFeatureMap.getMapperNameFromMethodName(statement.getId());
TranDB tranDB = mapperAuxFeatureMap.mapperTranDbMap.get(mapper);
if (statement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
db = tranDB.from();
} else {
db = tranDB.to();
}
DynamicDataSourceHolder.setDbType(db);
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
if (o instanceof Executor) {
return Plugin.wrap(o, this);
} else {
return o;
}
}
@Override
public void setProperties(Properties properties) {
}
}
然后對(duì)字段進(jìn)行修改的攔截器,這里為什么要繼承AbstactSqlPaserHandler呢,因?yàn)榭梢詮?fù)用他的方法,以及為后來加入表名替換的類做準(zhǔn)備,這里的流程是獲取原來字段的名字,并改為TranField的to所存儲(chǔ)的內(nèi)容
package com.trendy.task.transport.handler;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.trendy.task.transport.annotations.TranDB;
import com.trendy.task.transport.annotations.TranField;
import com.trendy.task.transport.config.MapperAuxFeatureMap;
import com.trendy.task.transport.util.CamelHumpUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
/**
* @author: lele
* @date: 2019/10/23 下午5:12
*/
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
),
@Signature(
type = StatementHandler.class,
method = "update",
args = {Statement.class}
),
@Signature(
type = StatementHandler.class,
method = "batch",
args = {Statement.class}
)
})
public class FieldHandler extends AbstractSqlParserHandler implements Interceptor {
private MapperAuxFeatureMap mapperAuxFeatureMap;
public FieldHandler(MapperAuxFeatureMap mapperAuxFeatureMap) {
this.mapperAuxFeatureMap = mapperAuxFeatureMap;
}
@Override
public Object plugin(Object target) {
return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
super.sqlParser(metaObject);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
Boolean select = mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT);
if (!select) {
//通過獲取mapper名稱從緩存類中獲取對(duì)應(yīng)的注解
String mapperName = MapperAuxFeatureMap.getMapperNameFromMethodName(mappedStatement.getId());
TranDB tranDB = mapperAuxFeatureMap.mapperTranDbMap.get(mapperName);
//獲取類的所有屬性
Class clazz = tranDB.object();
Map<String, Field> mapField = new HashMap<>(clazz.getFields().length);
while (!clazz.equals(Object.class)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
mapField.put(field.getName(), field);
}
clazz = clazz.getSuperclass();
}
//替換sql
String sql = boundSql.getSql();
for (Map.Entry<String, Field> entry : mapField.entrySet()) {
String sqlFieldName = CamelHumpUtils.humpToLine(entry.getKey());
if (sql.contains(sqlFieldName)) {
String from = entry.getValue().getAnnotation(TranField.class).to();
sql = sql.replaceAll(sqlFieldName, from);
}
}
metaObject.setValue("delegate.boundSql.sql", sql);
}
return invocation.proceed();
}
}
現(xiàn)在還有一個(gè)問題要處理,就是表名替換,但是這個(gè)有個(gè)小坑,這個(gè)功能也相當(dāng)于上面替換sql的功能比如insert into user(user_info,user_id) values ...,比如把user這個(gè)表名替換為user_info這個(gè)表來執(zhí)行,此時(shí)的插入語句會(huì)把所有user的替換成user_info,這時(shí)候官方的建議是用@TableName這個(gè)注解更改表名避免出現(xiàn)這個(gè)情況

表名處理器
使用:實(shí)現(xiàn)ITableNameHandler,并實(shí)現(xiàn)接口方法返回一個(gè)表名字
package com.trendy.task.transport.handler;
import com.baomidou.mybatisplus.extension.parsers.ITableNameHandler;
import com.trendy.task.transport.annotations.TranTable;
import org.apache.ibatis.reflection.MetaObject;
/**
* @author: lele
* @date: 2019/10/24 下午2:39
* 表名替換的handler
*/
public class SelfTableNameHandler implements ITableNameHandler {
private final Class clazz;
public SelfTableNameHandler(Class clazz) {
this.clazz = clazz;
}
@Override
public String dynamicTableName(MetaObject metaObject, String sql, String tableName) {
TranTable t = (TranTable) clazz.getAnnotation(TranTable.class);
if (sql.toLowerCase().startsWith("select")) {
return t.from();
} else {
return t.to();
}
}
}
也可以注入到mp自帶的分頁那個(gè)攔截器中
@Bean
public FieldHandler fieldHandler() {
FieldHandler f = new FieldHandler(mapperAuxFeatureMap());
DynamicTableNameParser t = new DynamicTableNameParser();
t.setTableNameHandlerMap(mapperAuxFeatureMap().tableNameHandlerMap);
f.setSqlParserList(Collections.singletonList(t));
return f;
}
字段填充器
mysql的表里有創(chuàng)建時(shí)間、修改時(shí)間、積分,對(duì)于這些字段,而pgsql表里面沒有,這時(shí)候想在插入時(shí)使用一個(gè)默認(rèn)的值,這時(shí)候可以使用字段填充器
做法:實(shí)現(xiàn)MetaObjectHandler接口,并重寫里面的方法,然后在字段的@TableField的fill類型里面說明需要填充時(shí)情況

例子,對(duì)createTime,updateTime,bouns進(jìn)行默認(rèn)填充,使用getFieldValueByName和setFieldValByName方法進(jìn)行賦值
package com.trendy.task.transport.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import java.util.Date;
/**
* @author lulu
* @Date 2019/10/24 23:15
* 自動(dòng)填充的handler
*/
public class DefaultFieldValueHandler implements MetaObjectHandler {
public static final String CREATETIME = "createTime";
public static final String UPDATETIME = "updateTime";
public static final String BOUNS = "bonus";
private void handle(String name, MetaObject metaObject, Object target) {
Object o = getFieldValByName(name, metaObject);
if (o == null) {
setFieldValByName(name, target, metaObject);
}
}
@Override
public void insertFill(MetaObject metaObject) {
handle(CREATETIME, metaObject, new Date());
handle(UPDATETIME, metaObject, new Date());
handle(BOUNS, metaObject, 500);
}
@Override
public void updateFill(MetaObject metaObject) {
handle(UPDATETIME, metaObject, new Date());
}
}
然后為user添加注解@TableField的注解
@TranField(from=TranField.empty,to="create_time")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TranField(from=TranField.empty,to="update_time")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TranField(from=TranField.empty,to="bonus")
@TableField(fill= FieldFill.INSERT)
private Integer bonus;
然后在全局配置中加入這個(gè)字段填充器類
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setBanner(false);
globalConfig.setMetaObjectHandler(defaultFieldValueHandler());
return globalConfig;
}
工廠類配置上全局配置 sqlSessionFactory.setGlobalConfig(globalConfig());
類型處理器
類型處理器,用于 JavaType 與 JdbcType 之間的轉(zhuǎn)換,用于 PreparedStatement 設(shè)置參數(shù)值和從 ResultSet 或 CallableStatement 中取出一個(gè)值,比如把String、Integer、Long、Double放到數(shù)據(jù)庫里面用逗號(hào)分隔形式存儲(chǔ),此時(shí)可以
自定義類型處理器,這里針對(duì)上面四個(gè)對(duì)象數(shù)組實(shí)現(xiàn)類型轉(zhuǎn)換處理,setxxx方法主要是對(duì)參數(shù)進(jìn)行處理,然后把處理后的結(jié)果放入數(shù)據(jù)庫中,而get方法則處理從數(shù)據(jù)庫取出來的數(shù)據(jù)該如何處理,這里接受一個(gè)lambda函數(shù)作為方法轉(zhuǎn)換,即字符串-》目標(biāo)類型,這里定義一個(gè)抽象類統(tǒng)一處理方法,然后具體轉(zhuǎn)換方法由子類實(shí)現(xiàn)
package com.trendy.task.transport.handler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.function.Function;
/**
* @author lulu
* @Date 2019/10/25 21:46
*/
//@MappedJdbcTypes({})表明處理哪種jdbc類型
@MappedTypes(Object[].class)//表明處理哪種javatype
public abstract class AbstractArrayTypeHandler<T> extends BaseTypeHandler<Object[]> {
//這里接受一個(gè)lambdah函數(shù)做轉(zhuǎn)換處理
private final Function<String,T> method;
public AbstractArrayTypeHandler(Function<String,T> method){
this.method=method;
}
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object[] objects, JdbcType jdbcType) throws SQLException {
StringBuilder sb=new StringBuilder();
for(Object o:objects){
sb.append(o.toString()+",");
}
sb.deleteCharAt(sb.length()-1);
preparedStatement.setString(i,sb.toString());
}
@Override
public T[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
return getArray(resultSet.getString(s));
}
@Override
public T[] getNullableResult(ResultSet resultSet, int i) throws SQLException {
return getArray(resultSet.getString(i));
}
@Override
public Object[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return getArray(callableStatement.getString(i));
}
protected T[] getArray(String source){
if(source==null){
return null;
}
String[] resString=source.split(",");
if(this.method==null){
return (T[])resString;
}
T[] resArray= (T[]) new Object[resString.length];
for (int i = 0; i < resString.length; i++) {
resArray[i]=method.apply(resString[i]);
}
return resArray;
}
}
然后定義一個(gè)工廠存放子類
package com.trendy.task.transport.handler;
import java.util.function.Function;
/**
* @author lulu
* @Date 2019/10/25 22:33
*/
public interface ArrayTypeHandlerFactory {
class IntegerArrayTypeHandler extends AbstractArrayTypeHandler<Integer>{
public IntegerArrayTypeHandler() {
super(Integer::parseInt);
}
}
class DoubleArrayTypeHandler extends AbstractArrayTypeHandler<Double>{
public DoubleArrayTypeHandler(){
super(Double::parseDouble);
}
}
class LongArrayTypeHandler extends AbstractArrayTypeHandler<Long>{
public LongArrayTypeHandler(){
super(Long::parseLong);
}
}
class StringArrayTypeHandler extends AbstractArrayTypeHandler<String>{
public StringArrayTypeHandler(){
super(null);
}
}
}
指定處理類型
@TableField(typeHandler = ArrayTypeHandlerFactory.StringArrayTypeHandler.class)
好了,現(xiàn)在來測(cè)試下,
@Test
public void selectById() {
List<User> userList = userService.list(new LambdaQueryWrapper<User>().select(User::getUserId,User::getMobile,User::getUserAccount,User::getTest));
userService.saveBatch(userList);
}



大概就到這里,代碼完整版的github地址:GitHub - 97lele/transport
但是這個(gè)有個(gè)缺陷,就是不支持事務(wù),還有saveOrUpdate方法也不支持,因?yàn)閮蓚€(gè)數(shù)據(jù)源都不一樣,他是先查,看是否有再做更新或者插入操作,這些問題仍需解決,且當(dāng)作一個(gè)使用方法記錄的小例子吧
補(bǔ)充
mybatis-plus默認(rèn)的baseMapper不支持批量的插入和更新,有時(shí)候會(huì)為了這個(gè)批量的方法去繼承serviceImpl類,而這個(gè)類有沒有其他的業(yè)務(wù)代碼在里面,繼承僅僅是為了獲取批量操作的方法,會(huì)顯得這個(gè)類有點(diǎn)”貧血“
對(duì)于這種情況,偶然看到一個(gè)開源的項(xiàng)目onemall對(duì)BaseMapper做了擴(kuò)展,使它支持了批量插入的做法,在此基礎(chǔ)下,我也做了一定的擴(kuò)展
根據(jù)自己的見解,簡(jiǎn)單描述下流程:
核心就是自定義一個(gè)sql自動(dòng)注入器,mybatis-plus會(huì)掃描mapper類 ,為每個(gè)mapper類構(gòu)造一個(gè)MappedStatement(相當(dāng)于mapper里的一個(gè)sql)

添加到配置類里面(具體在下面(2)),只會(huì)在初始化時(shí)候執(zhí)行一次,而我們要做的,就是把某個(gè)特定mapper,注入我們想要的模板方法
具體注入在MybatisMapperAnnotationBuilder,注入動(dòng)態(tài)sql,getSqlInjector獲取sql注入器,并為當(dāng)前的MapperBuilderAssistant對(duì)象(類似于mapper)注入mappedStatement(sql)
(1)注入開始,里面的inspectInject就是

inspectInject就會(huì)把a(bǔ)bstractMethod一一注入進(jìn)去
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
if (CollectionUtils.isNotEmpty(methodList)) {
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 循環(huán)注入自定義方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
((2)抽象方法注入類,最終放到configuration類緩存,形成一個(gè)sql

具體看看AbstractMethod的實(shí)現(xiàn)類,而默認(rèn)的DefaultSqlInject類就有我們baseMapper所包含的默認(rèn)方法,我們要添加多兩個(gè)方法,批量更新和批量插入

下面開始編碼
首先繼承AbstarctMethod類,實(shí)現(xiàn)我們想注入的sql模板
對(duì)于批量插入,使用 insert into table columns values (values),(values),(values)
對(duì)于批量更新,使用 update table set xx=xx,xx=xx where xx=xx;update table set xx=xx,xx=xx where xx=xx;update table set xx=xx,xx=xx where xx=xx;進(jìn)行分號(hào)分割
因?yàn)橐玫綐?biāo)簽解析的功能,需要用script包裹起來。

具體初始化模板如下, 對(duì)其進(jìn)行填充
public enum CustomSqlMethodEnum {
/**
* 批量插入
*/
INSERT_BATCH("insertBatch",
"批量插入",
"<script>\n"
+ "INSERT INTO %s %s VALUES \n"
+ "<foreach collection=\"collection\" item=\"item\" separator=\",\"> %s\n </foreach>\n"
+ "</script>"),
/**
* 批量更新
*/
UPDATE_BATCH("updateBatchByIds",
"批量更新",
"<script>\n" +
"<foreach collection=\"collection\" item=\"item\" separator=\";\"> update %s set %s where %s </foreach>\n"
+ "</script>"
),
/**
* 根據(jù)聯(lián)合鍵查詢
*/
SELECT_BY_COMPOSEKEYS("selectByComposeKeys",
"聯(lián)合主鍵查詢",
"<script>" +
" select <choose> <when test=\"ew!=null and ew.sqlSelect != null and ew.sqlSelect != ''\"> ${ew.sqlSelect} </when> <otherwise> * </otherwise> </choose> from %s "
+ "<where> <foreach collection=\"collection\" item=\"item\" open=\"(\" close=\")\" separator=\"or\"> ( %s ) </foreach> <if test=\"ew!=null and ew.sqlSegment != null and ew.sqlSegment != ''\">\n" +
"AND ${ew.sqlSegment}\n" +
"</if> </where> </script>"
),
/**
* 根據(jù)聯(lián)合鍵查詢
*/
SELECT_IDS_BY_COMPOSEKEYS("selectIdsByComposeKeys",
"聯(lián)合主鍵查詢id",
"<script>" +
" select %s from %s "
+ "<where> <foreach collection=\"collection\" item=\"item\" open=\"(\" close=\")\" separator=\"or\"> ( %s ) </foreach> </where> </script>"
),
/**
* 根據(jù)聯(lián)合主鍵刪除
*/
DELETE_BY_COMPOSEKEYS("deleteByComposeKeys",
"聯(lián)合主鍵刪除",
"<script>" +
" delete from %s "
+ "<where> <foreach collection=\"collection\" item=\"item\" open=\"(\" close=\")\" separator=\"or\"> ( %s ) </foreach> </where> </script>"
),
/**
* 根據(jù)聯(lián)合主鍵更新
*/
UPDATE_BY_COMPOSEKEYS("updateByComposeKeys"
, "聯(lián)合主鍵批量修改",
"<script>\n" +
"<foreach collection=\"collection\" item=\"item\" separator=\";\"> update %s set %s where %s </foreach>\n"
+ "</script>"
);
private final String method;
private final String desc;
private final String sql;
CustomSqlMethodEnum(String method, String desc, String sql) {
this.method = method;
this.desc = desc;
this.sql = sql;
}
public String getMethod() {
return method;
}
public String getDesc() {
return desc;
}
public String getSql() {
return sql;
}
}
下面是構(gòu)造批量插入和更新的代碼邏輯
批量插入:主要是構(gòu)造——表名、插入列、插入值,這里為了防止覆蓋數(shù)據(jù)庫默認(rèn)值,也可以做if test 的那種判空,來取消插入,不過比較麻煩,這里就不做了
public class InsertBatch extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.INSERT_BATCH;
// ==== 拼接 sql 模板 ==============
StringBuilder columnScriptBuilder = new StringBuilder(LEFT_BRACKET);
StringBuilder valuesScriptBuilder = new StringBuilder(LEFT_BRACKET);
// 主鍵拼接
if (StringUtils.isNotBlank(tableInfo.getKeyColumn())) {
//(x1,x2,x3
columnScriptBuilder.append(tableInfo.getKeyColumn()).append(COMMA);
//(item.xx,item.xx
valuesScriptBuilder.append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + tableInfo.getKeyProperty())).append(COMMA);
}
// 普通字段拼接
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
for (TableFieldInfo fieldInfo : fieldList) {
//有更新默認(rèn)填充器的不參與賦值
if (!fieldInfo.isWithInsertFill() && fieldInfo.isWithUpdateFill()) {
continue;
}
columnScriptBuilder.append(fieldInfo.getColumn()).append(COMMA);
valuesScriptBuilder.append(SqlScriptUtils.safeParam(SQLConditionWrapper.getCondition(fieldInfo.getProperty()).toString())).append(COMMA);
}
// 替換多余的逗號(hào)為括號(hào)
//(x1,x2)
columnScriptBuilder.setCharAt(columnScriptBuilder.length() - 1, ')');
//(item.xx,item.xx2)
valuesScriptBuilder.setCharAt(valuesScriptBuilder.length() - 1, ')');
// sql 模板占位符替換
String columnScript = columnScriptBuilder.toString();
String valuesScript = valuesScriptBuilder.toString();
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
// === mybatis 主鍵邏輯處理:主鍵生成策略,以及主鍵回填=======
String keyColumn = null;
String keyProperty = null;
// 表包含主鍵處理邏輯,如果不包含主鍵當(dāng)普通字段處理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/** 自增主鍵 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(sqlMethod.getMethod(), tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
// 模板寫入
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource, keyGenerator, keyProperty, keyColumn);
}
}
更新sql構(gòu)造,這里要注意字段上的字段策略(FieldStrategy),附上處理類
public class UpdateBatchByIds extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.UPDATE_BATCH;
/**
* update table set <if test ="item.pro!=null">key1=#{item.pro}</if>, key2=#{item.pro2} where keycolom = %s
* */
StringBuilder withChevScript = new StringBuilder();
StringBuilder lastFiledScriptBuilder=new StringBuilder();
StringBuilder keyScriptBuilder = new StringBuilder();
if (StringUtils.isNotBlank(tableInfo.getKeyColumn())) {
keyScriptBuilder.append(tableInfo.getKeyColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + tableInfo.getKeyProperty()));
}
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
for (int i = 0; i < fieldList.size(); i++) {
TableFieldInfo fieldInfo = fieldList.get(i);
Boolean isLast=(i==(fieldList.size()-1));
//有插入默認(rèn)填充器的不參與賦值
if (fieldInfo.isWithInsertFill() && !fieldInfo.isWithUpdateFill()) {
continue;
}
boolean change = false;
if (Objects.equals(fieldInfo.getUpdateStrategy(), FieldStrategy.NOT_NULL) && !fieldInfo.isWithUpdateFill()) {
SQLConditionWrapper.appendNotNull(withChevScript, fieldInfo.getProperty());
change = true;
}
if (Objects.equals(fieldInfo.getUpdateStrategy(), FieldStrategy.NOT_EMPTY) && !fieldInfo.isWithUpdateFill()) {
SQLConditionWrapper.appendNotEmpty(withChevScript, fieldInfo.getProperty());
change = true;
}
if (change) {
withChevScript.append(fieldInfo.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.getCondition(fieldInfo.getProperty()).toString()));
withChevScript.append(COMMA).append("</if>");
if(isLast&&lastFiledScriptBuilder.length()==0){
//如果沒有其他字段替補(bǔ),弄個(gè)占位的,不然會(huì)出現(xiàn)語法錯(cuò)誤的情況
withChevScript.append(tableInfo.getKeyColumn()).append(EQUALS).append(tableInfo.getKeyColumn());
}
}else{
lastFiledScriptBuilder.append(fieldInfo.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.getCondition(fieldInfo.getProperty()).toString())).append(COMMA);
}
}
//處理多余的逗號(hào)
if(!StringUtils.isBlank(lastFiledScriptBuilder)&&!StringUtils.isBlank(withChevScript)){
int leftChevIndex= withChevScript.lastIndexOf(LEFT_CHEV);
if(withChevScript.charAt(leftChevIndex-1)!=','){
withChevScript.replace(leftChevIndex,leftChevIndex+1,",<");
}
if(lastFiledScriptBuilder.lastIndexOf(COMMA)==lastFiledScriptBuilder.length()-1){
lastFiledScriptBuilder.deleteCharAt(lastFiledScriptBuilder.length()-1);
}
}
withChevScript.append(lastFiledScriptBuilder);
// sql 模板占位符替換
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), withChevScript, keyScriptBuilder);
// 模板寫入
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);
}
}
public class SQLConditionWrapper {
public final static String ITEM = "item";
public static StringBuilder appendNotNull(StringBuilder builder, String property) {
return appendEnd(appendStart(builder, getCondition(property)));
}
public static StringBuilder appendNotEmpty(StringBuilder builder, String property) {
StringBuilder condition = getCondition(property);
StringBuilder append = appendStart(builder, condition)
.append(" ").append(AND).append(" ").append(condition).append(EXCLAMATION_MARK).append(EQUALS).append(" ").append("''");
return appendEnd(append);
}
private static StringBuilder appendEnd(StringBuilder builder) {
builder.append("\"")
.append(RIGHT_CHEV);
return builder;
}
private static StringBuilder appendStart(StringBuilder builder, StringBuilder item) {
builder.append(LEFT_CHEV)
.append("if test=\"")
.append(item).append(EXCLAMATION_MARK).append(EQUALS).append(NULL);
return builder;
}
public static StringBuilder getCondition(String property) {
return new StringBuilder()
.append(ITEM).append(DOT).append(property);
}
}
然后就自定義mapper及sql注入器
public interface CustomMapper<T> extends BaseMapper<T> {
/**
* 批量插入
* @param collection 批量插入數(shù)據(jù)
* @return ignore
*/
int insertBatch(@Param("collection") Collection<T> collection);
/**
* 批量更新
* @param collection
* @return
*/
int updateBatchByIds(@Param("collection") Collection<T> collection);
}
這里sql注入器要判斷下是繼承了該類,才進(jìn)行方法擴(kuò)展
@ConditionalOnExpression("${mybatis-plus.custom-mapper.enabled:true}")
@Component
public class CustomSqlInject extends DefaultSqlInjector {
//初始化時(shí)候會(huì)加載
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
//屬于自己定義的mapper才添加方法
if (((ParameterizedTypeImpl) mapperClass.getGenericInterfaces()[0]).getRawType().equals(CustomMapper.class)) {
methodList.add(new InsertBatch());
methodList.add(new UpdateBatchByIds());
}
if (((ParameterizedTypeImpl) mapperClass.getGenericInterfaces()[0]).getRawType().equals(ComposeKeyMapper.class)) {
methodList.add(new SelectByComposeKeys());
methodList.add(new SelectIdsByComposeKeys());
methodList.add(new UpdateByComposeKeys());
methodList.add(new InsertBatch());
}
return methodList;
}
}
可以直接注入到spring里面,或者通過進(jìn)行設(shè)置
GlobalConfigUtils.getGlobalConfig(sqlSessionFactory.getConfiguration()).setSqlInjector(xx)
測(cè)試一下,定義實(shí)體,下面的typeHandler可以去掉,
@TableName(
value = "demo"
)
@Data
@Accessors(chain = true)
public class Demo extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId("id")
private Long id;
@TableField("name")
private String name;
@TableField(
value = "age",
typeHandler = LongFormatAESEncryptHandler.class
)
private Long age;
@TableField(
value = "secret",
typeHandler = StringFormatAESEncryptHandler.class
)
private String secret;
@TableField(
value = "CREATED_ID",
fill = FieldFill.INSERT
)
private Long createdId;
@TableField(
value = "CREATED_BY",
fill = FieldFill.INSERT
)
private String createdBy;
@TableField(
value = "CREATION_DATE",
fill = FieldFill.INSERT
)
private Date creationDate;
@TableField(
value = "CREATED_BY_IP",
fill = FieldFill.INSERT
)
private String createdByIp;
@TableField(
value = "LAST_UPDATED_ID",
fill = FieldFill.UPDATE
)
private Long lastUpdatedId;
@TableField(
value = "LAST_UPDATED_BY",
fill = FieldFill.UPDATE
)
private String lastUpdatedBy;
@TableField(
value = "LAST_UPDATE_DATE",
fill = FieldFill.INSERT_UPDATE
)
private Date lastUpdateDate;
@TableField(
value = "LAST_UPDATED_BY_IP",
fill = FieldFill.UPDATE
)
private String lastUpdatedByIp;
}
繼承自定義mapper
public interface TempMapper extends CustomMapper<Demo> {
}
controller了的兩個(gè)方法,可以支持事務(wù)
@GetMapping("/testBatch")
@Transactional(rollbackFor = Exception.class)
public void testBatch() {
List<Demo> demos = new LinkedList<>();
for (int i = 0; i < 10; i++) {
Demo de = new Demo().setAge(Long.valueOf(i))
.setId(Long.valueOf(i + 1))
.setName("test" + i)
.setSecret(UUID.randomUUID().toString());
demos.add(de);
}
tempMapper.insertBatch(demos);
}
@GetMapping("/testBatch2")
@Transactional(rollbackFor = Exception.class)
public void testBatch2() {
List<Demo> demos = new LinkedList<>();
for (int i = 0; i < 5; i++) {
Demo de = new Demo().setAge(Long.valueOf(i))
.setId(Long.valueOf(i + 1))
.setName("test-change" + i)
.setSecret(UUID.randomUUID().toString());
demos.add(de);
}
tempMapper.updateBatchByIds(demos);
//int i=1/0;
}
對(duì)于自定sql注入,還可以抽取一些公用的方法進(jìn)行添加,比如邏輯刪除,版本號(hào)更新,進(jìn)行聯(lián)合主鍵的批量修改和查詢,下面給出聯(lián)合主鍵的處理方案
下面是構(gòu)造根據(jù)wrapper和聯(lián)合主鍵進(jìn)行查詢的,核心也是定義好sql,然后進(jìn)行注入,但這里需要自定義一個(gè)注解來標(biāo)識(shí)哪個(gè)是聯(lián)合主鍵
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ComposeKey {
}
public class SelectByComposeKeys extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.SELECT_BY_COMPOSEKEYS;
//select #{ew.sqlSelect} from table_name where (composekey1=xx and composekey2=xx) or (composekey1=xx1 and composekey2=xx1)
String sqlTemplate = sqlMethod.getSql();
String tableName = tableInfo.getTableName();
List<TableFieldInfo> composeKeys = tableInfo.getFieldList().stream().filter(e -> e.getField().isAnnotationPresent(ComposeKey.class))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(composeKeys)){
throw new ApiException("not composeKey found in class:"+modelClass.getName());
}
StringBuilder builder=new StringBuilder();
for (int i = 0; i < composeKeys.size(); i++) {
TableFieldInfo composeKey = composeKeys.get(i);
builder.append(composeKey.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + composeKey.getProperty()));
if(i!=composeKeys.size()-1){
builder.append(" ").append(AND).append(" ");
}
}
String finalSql = String.format(sqlTemplate, tableName, builder);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, finalSql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass,sqlMethod.getMethod(),sqlSource,tableInfo);
}
}
更新操作
public class UpdateByComposeKeys extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.UPDATE_BY_COMPOSEKEYS;
/**
* update table set <if test ="item.pro!=null">key1=#{item.pro}</if>, key2=#{item.pro2} where keycolom = %s
*/
StringBuilder withChevScript = new StringBuilder();
StringBuilder lastFiledScriptBuilder = new StringBuilder();
StringBuilder keyScriptBuilder = new StringBuilder();
StringBuilder placeHolder = new StringBuilder();
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
for (int i = 0; i < fieldList.size(); i++) {
TableFieldInfo fieldInfo = fieldList.get(i);
Boolean isLast = (i == (fieldList.size() - 1));
if (fieldInfo.getField().isAnnotationPresent(ComposeKey.class)) {
keyScriptBuilder.append(" ").append(fieldInfo.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + fieldInfo.getProperty()))
.append(" ").append(AND);
placeHolder.append(fieldInfo.getColumn()).append(EQUALS).append(fieldInfo.getColumn()).append(COMMA);
continue;
}
//有插入默認(rèn)填充器或者主鍵的不參與賦值
if (fieldInfo.isWithInsertFill() && !fieldInfo.isWithUpdateFill()) {
continue;
}
boolean change = false;
if (Objects.equals(fieldInfo.getUpdateStrategy(), FieldStrategy.NOT_NULL) && !fieldInfo.isWithUpdateFill()) {
SQLConditionWrapper.appendNotNull(withChevScript, fieldInfo.getProperty());
change = true;
}
if (Objects.equals(fieldInfo.getUpdateStrategy(), FieldStrategy.NOT_EMPTY) && !fieldInfo.isWithUpdateFill()) {
SQLConditionWrapper.appendNotEmpty(withChevScript, fieldInfo.getProperty());
change = true;
}
if (change) {
withChevScript.append(fieldInfo.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.getCondition(fieldInfo.getProperty()).toString()));
withChevScript.append(COMMA).append("</if>");
if (isLast && lastFiledScriptBuilder.length() == 0) {
//如果沒有其他字段替補(bǔ),弄個(gè)占位的,不然會(huì)出現(xiàn)語法錯(cuò)誤的情況
if (placeHolder.length() > 0) {
//刪除多余的逗號(hào)
placeHolder.deleteCharAt(placeHolder.length() - 1);
withChevScript.append(placeHolder);
}
}
} else {
lastFiledScriptBuilder.append(fieldInfo.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.getCondition(fieldInfo.getProperty()).toString())).append(COMMA);
}
}
if (placeHolder.length() == 0) {
throw new ApiException("not composeKey found in class:" + modelClass.getName());
}
//處理多余的逗號(hào)
if (!StringUtils.isBlank(lastFiledScriptBuilder) && !StringUtils.isBlank(withChevScript)) {
int leftChevIndex = withChevScript.lastIndexOf(LEFT_CHEV);
if (withChevScript.charAt(leftChevIndex - 1) != ',') {
withChevScript.replace(leftChevIndex, leftChevIndex + 1, ",<");
}
if (lastFiledScriptBuilder.lastIndexOf(COMMA) == lastFiledScriptBuilder.length() - 1) {
lastFiledScriptBuilder.deleteCharAt(lastFiledScriptBuilder.length() - 1);
}
}
if(!StringUtils.isBlank(keyScriptBuilder)){
int i = keyScriptBuilder.lastIndexOf(AND);
keyScriptBuilder.delete(i,i+AND.length());
}
withChevScript.append(lastFiledScriptBuilder);
// sql 模板占位符替換
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), withChevScript, keyScriptBuilder);
// 模板寫入
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);
}
}
public class SelectIdsByComposeKeys extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.SELECT_IDS_BY_COMPOSEKEYS;
//select composekeys from table_name where (composekey1=xx and composekey2=xx) or (composekey1=xx1 and composekey2=xx1)
String sqlTemplate = sqlMethod.getSql();
String tableName = tableInfo.getTableName();
List<TableFieldInfo> composeKeys = tableInfo.getFieldList().stream().filter(e -> e.getField().isAnnotationPresent(ComposeKey.class))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(composeKeys)){
throw new ApiException("not composeKey found in class:"+modelClass.getName());
}
StringBuilder builder=new StringBuilder();
StringBuilder select=new StringBuilder();
for (int i = 0; i < composeKeys.size(); i++) {
TableFieldInfo composeKey = composeKeys.get(i);
select.append(composeKey.getColumn());
builder.append(composeKey.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + composeKey.getProperty()));
if(i!=composeKeys.size()-1){
builder.append(" ").append(AND).append(" ");
select.append(COMMA);
}
}
String finalSql = String.format(sqlTemplate,select, tableName, builder);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, finalSql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass,sqlMethod.getMethod(),sqlSource,tableInfo);
}
}
public class DeleteByComposeKeys extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
CustomSqlMethodEnum sqlMethod = CustomSqlMethodEnum.DELETE_BY_COMPOSEKEYS;
//delete from table_name where (composekey1=xx and composekey2=xx) or (composekey1=xx1 and composekey2=xx1)
String sqlTemplate = sqlMethod.getSql();
String tableName = tableInfo.getTableName();
List<TableFieldInfo> composeKeys = tableInfo.getFieldList().stream().filter(e -> e.getField().isAnnotationPresent(ComposeKey.class))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(composeKeys)){
throw new ApiException("not composeKey found in class:"+modelClass.getName());
}
StringBuilder builder=new StringBuilder();
for (int i = 0; i < composeKeys.size(); i++) {
TableFieldInfo composeKey = composeKeys.get(i);
builder.append(composeKey.getColumn()).append(EQUALS).append(SqlScriptUtils.safeParam(SQLConditionWrapper.ITEM + DOT + composeKey.getProperty()));
if(i!=composeKeys.size()-1){
builder.append(" ").append(AND).append(" ");
}
}
String finalSql = String.format(sqlTemplate, tableName, builder);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, finalSql, modelClass);
return this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);
}
}
定義mappe類,這里對(duì)于原生的方法就不進(jìn)行支持了
public interface ComposeKeyMapper<T> extends CustomMapper<T> {
/**
* @param params 查詢的主鍵入?yún)?
* @param wrapper 查詢的列會(huì)用到 ew.sqlSelect
* @return
*/
List<T> selectByComposeKeys(@Param("collection") Collection<T> params, @Param(Constants.WRAPPER) Wrapper<T> wrapper);
/**
* 批量更新
*
* @param params
*/
void updateByComposeKeys(@Param("collection") Collection<T> params);
/**
* 只查詢主鍵
*
* @param params
* @return
*/
List<T> selectIdsByComposeKeys(@Param("collection") Collection<T> params);
/**
* 聯(lián)合主鍵刪除
* @param params
* @return
*/
int deleteByComposeKeys(@Param("collection")Collection<T> params);
/**
* 下面的方法不支持
* @param entity
* @return
*/
@Override
default int updateById(T entity) {
throw new UnsupportedOperationException();
}
@Override
default int updateBatchByIds(Collection<T> collection) {
throw new UnsupportedOperationException();
}
@Override
default T selectById(Serializable id) {
throw new UnsupportedOperationException();
}
@Override
default List<T> selectBatchIds(Collection<? extends Serializable> idList) {
throw new UnsupportedOperationException();
}
@Override
default int deleteBatchIds(Collection<? extends Serializable> idList){
throw new UnsupportedOperationException();
}
@Override
default int deleteById(Serializable id){
throw new UnsupportedOperationException();
}
}

![]()
![]()
可以看到查詢是按照預(yù)期給出的,也支持wrapper形式的查詢
要注意,xml文件里的優(yōu)先級(jí)是比自定義注入的優(yōu)先級(jí)高的,這里沒給出單個(gè)update,單個(gè)查詢,單個(gè)刪除的方法,這些都可以根據(jù)自己業(yè)務(wù)需求弄一下,項(xiàng)目上沒用到就不寫了
對(duì)于sqlSession也可以自己做一些小優(yōu)化
類型有reuse,編譯一次可以繼續(xù)服用,使用場(chǎng)景:執(zhí)行同一個(gè)sql,參數(shù)值不同
默認(rèn)的simple是每次都會(huì)編譯一下
batch相當(dāng)于一個(gè)會(huì)話里面執(zhí)行多條sql
reuse示例
public static void executeReuse(Consumer<SqlSession> consumer) {
SqlSessionFactory factory = SpringContextHolder.getBean(SqlSessionFactory.class);
SqlSession sqlSession = factory.openSession(ExecutorType.REUSE, true);
try {
consumer.accept(sqlSession);
} finally {
sqlSession.close();
}
}
到此這篇關(guān)于mybatis-plus攔截器、字段填充器、類型處理器、表名替換、SqlInjector(聯(lián)合主鍵處理)的文章就介紹到這了,更多相關(guān)mybatis-plus攔截器、字段填充器、類型處理器、表名替換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MybatisPlus攔截器如何實(shí)現(xiàn)數(shù)據(jù)表分表
- mybatis-plus配置攔截器實(shí)現(xiàn)sql完整打印的代碼設(shè)計(jì)
- MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的方法
- Mybatis-plus通過添加攔截器實(shí)現(xiàn)簡(jiǎn)單數(shù)據(jù)權(quán)限
- MybatisPlusInterceptor實(shí)現(xiàn)sql攔截器超詳細(xì)教程
- MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的示例
- mybatis-plus 攔截器敏感字段加解密的實(shí)現(xiàn)
- MyBatis-Plus攔截器對(duì)敏感數(shù)據(jù)實(shí)現(xiàn)加密
- mybatisplus 的SQL攔截器實(shí)現(xiàn)關(guān)聯(lián)查詢功能
- Mybatis Plus 3.4.0分頁攔截器的用法小結(jié)
相關(guān)文章
SpringBoot實(shí)現(xiàn)License認(rèn)證(只校驗(yàn)有效期)的詳細(xì)過程
License也就是版權(quán)許可證書,一般用于收費(fèi)軟件給付費(fèi)用戶提供的訪問許可證明,這篇文章主要介紹了SpringBoot實(shí)現(xiàn)License認(rèn)證(只校驗(yàn)有效期),需要的朋友可以參考下2024-04-04
java圖片滑動(dòng)驗(yàn)證(登錄驗(yàn)證)原理與實(shí)現(xiàn)方法詳解
這篇文章主要介紹了java圖片滑動(dòng)驗(yàn)證(登錄驗(yàn)證)原理與實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了java圖片滑動(dòng)登錄驗(yàn)證的相關(guān)原理、實(shí)現(xiàn)方法與操作技巧,需要的朋友可以參考下2019-09-09
Spring中property-placeholder的使用與解析詳解
本篇文章主要介紹了Spring中property-placeholder的使用與解析詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
8個(gè)簡(jiǎn)單部分開啟Java語言學(xué)習(xí)之路 附j(luò)ava學(xué)習(xí)書單
8個(gè)簡(jiǎn)單部分開啟Java語言學(xué)習(xí)之路,附j(luò)ava學(xué)習(xí)書單,這篇文章主要向大家介紹了學(xué)習(xí)java語言的方向,感興趣的小伙伴們可以參考一下2016-09-09
jpa?EntityManager?復(fù)雜查詢實(shí)例
這篇文章主要介紹了jpa?EntityManager?復(fù)雜查詢實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
使用spring-task定時(shí)任務(wù)動(dòng)態(tài)配置修改執(zhí)行時(shí)間
這篇文章主要介紹了使用spring-task定時(shí)任務(wù)動(dòng)態(tài)配置修改執(zhí)行時(shí)間,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
如何保證RabbitMQ全鏈路數(shù)據(jù)100%不丟失問題
這篇文章主要介紹了如何保證RabbitMQ全鏈路數(shù)據(jù)100%不丟失問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05

