MyBatis-Plus - 一篇带你玩转自定义 BaseMapper
Posted 程序员牧码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis-Plus - 一篇带你玩转自定义 BaseMapper相关的知识,希望对你有一定的参考价值。
业务背景
在使用 @TableLogic (逻辑删)注解的基础上,提供一个全局方法,使得插入数据的时候,一旦发现有相同主键 Id 的记录就先删除再插入(系统搬家的时候遇到。因为系统搬家产生这个问题的场景:先导入搬家的数据,然后在目标系统删除该数据[假删除],再次重复导入就会 SQL 物理主键冲突报错)~
解决方案
实现一个插入方法,在插入的时候,先删除该记录 Id 并且 del == 1(假删除标记),然后再插入!
实现思路
在玩 MP 的时候我们都知道 BaseMapper,那么优秀的框架一定会给我们扩展的机会,果真如此
1、【DBaseMapper】实现一个自定义 Mapper 继承 BaseMapper 来替代 BaseMapper 在 Dao 的继承上
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import java.lang.reflect.Field;
import java.util.Objects;
/**
* @author Lux Sun
* @date 2022/1/14
*/
public interface DBaseMapper<T> extends BaseMapper<T>
Integer IS_DEL = 1;
/**
* 插入一条记录(解决主键冲突)
*
* @param entity
*/
default int insertWithPrimary(T entity)
// 主键 Id
Field[] fields = entity.getClass().getDeclaredFields();
// 删除条件
QueryWrapper<T> qw = new QueryWrapper<>();
String fieldName;
for (Field field : fields)
fieldName = field.getName();
// 装配逻辑删字段 == 1
if (Objects.nonNull(field.getAnnotation(TableLogic.class)))
qw.eq(StrUtil.toUnderlineCase(fieldName), IS_DEL);
// 装配主键 Id
else if (Objects.nonNull(field.getAnnotation(TableId.class)))
try
field.setAccessible(true);
qw.eq(StrUtil.toUnderlineCase(fieldName), field.get(entity));
catch (IllegalAccessException e)
e.printStackTrace();
this.deletePhysically(qw);
return this.insert(entity);
/**
* 根据 entity 条件, 删除记录(物理删除)
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int deletePhysically(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
2、因为插入的方法 BaseMapper 本身自带,但是没有一个方法可以在 @TableLogic 注解的基础上删除,等下给几段官方源码你就明白了,因为 MyBatis 只要识别出 POJO 有该注解,就会对一系列必要地方进行特判
- TableInfo.java
- 该类可以简单理解为和你的 POJO 有着千丝万缕的关系,言外之意你在 POJO 里的属性配置啥的都会在该类里更详细的体现出来
package com.baomidou.mybatisplus.core.metadata;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.session.Configuration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import static java.util.stream.Collectors.joining;
/**
* 数据库表反射信息
*
* @author hubin
* @since 2016-01-23
*/
@Data
@Setter(AccessLevel.PACKAGE)
@Accessors(chain = true)
public class TableInfo implements Constants
/**
* 是否开启逻辑删除
*/
@Setter(AccessLevel.NONE)
private boolean logicDelete;
/**
* 设置逻辑删除
*/
void setLogicDelete(boolean logicDelete)
if (logicDelete)
this.logicDelete = true;
/**
* 获取逻辑删除字段的 sql 脚本
*
* @param startWithAnd 是否以 and 开头
* @param deleteValue 是否需要的是逻辑删除值
* @return sql 脚本
*/
public String getLogicDeleteSql(boolean startWithAnd, boolean deleteValue)
if (logicDelete)
TableFieldInfo field = fieldList.stream().filter(TableFieldInfo::isLogicDelete).findFirst()
.orElseThrow(() -> ExceptionUtils.mpe("can't find the logicFiled from table %s", tableName));
String logicDeleteSql = formatLogicDeleteSql(field, deleteValue);
if (startWithAnd)
logicDeleteSql = " AND " + logicDeleteSql;
return logicDeleteSql;
return EMPTY;
// 省略其他代码...
- Delete.java
- 因为我们参考的是该方法的代码来进行编码彻底删除的逻辑,所以我们先看下原先该方法的模样
/*
* Copyright (c) 2011-2020, baomidou (jobob@qq.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.baomidou.mybatisplus.core.injector.methods;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 根据 entity 条件删除记录
*
* @author hubin
* @since 2018-04-06
*/
public class Delete extends AbstractMethod
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)
String sql;
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
// 使用 logicDelete 进行判断
if (tableInfo.isLogicDelete())
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
else
sqlMethod = SqlMethod.DELETE;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
- AbstractMethod.java - sqlWhereEntityWrapper 函数中的 TableInfo.getLogicDeleteSql 函数
- sqlWhereEntityWrapper 该函数是实现对 SQL 语句 WHERE 的生成,所以会涉及到 “DEL = ?” 的逻辑
package com.baomidou.mybatisplus.core.injector;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
/**
* 抽象的注入方法类
*
* @author hubin
* @since 2018-04-06
*/
public abstract class AbstractMethod implements Constants
/**
* EntityWrapper方式获取select where
*
* @param newLine 是否提到下一行
* @param table 表信息
* @return String
*/
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table)
if (table.isLogicDelete())
String sqlScript = table.getAllSqlWhere(true, true, WRAPPER_ENTITY_DOT);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY),
true);
// 关键代码点
sqlScript += (NEWLINE + table.getLogicDeleteSql(true, false) + NEWLINE);
String normalSqlScript = SqlScriptUtils.convertIf(String.format("AND $%s", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_NONEMPTYOFNORMAL), true);
normalSqlScript += NEWLINE;
normalSqlScript += SqlScriptUtils.convertIf(String.format(" $%s", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_EMPTYOFNORMAL), true);
sqlScript += normalSqlScript;
sqlScript = SqlScriptUtils.convertChoose(String.format("%s != null", WRAPPER), sqlScript,
table.getLogicDeleteSql(false, false));
sqlScript = SqlScriptUtils.convertWhere(sqlScript);
return newLine ? NEWLINE + sqlScript : sqlScript;
else
String sqlScript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
sqlScript += NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " $%s", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_NONEMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(" $%s", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_EMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
return newLine ? NEWLINE + sqlScript : sqlScript;
// 省略其他代码...
Ps:以上几段框架中的代码都能说明 @TableLogic 注解的重要性,但同样是我们的要解决的难点,废话不多说了,看下面我们的解决方案
3、【DeletePhysically】我们在第 1 个步骤的基础上,实现我们自定义方法的逻辑
/**
* 根据 entity 条件, 删除记录(物理删除)
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int deletePhysically(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.lang.reflect.Field;
/**
* @author Lux Sun
* @date 2022/1/14
*/
public class DeletePhysically extends AbstractMethod
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)
// (1) DELETE FROM %s %s %s
// (2) DELETE FROM t_test WHERE test_del=0 AND (id = ? AND test_del = ?)
DSqlMethod dSqlMethod = DSqlMethod.DELETE;
// 反射修改 logicDelete = false 否则生成 (2) 代码
try
Field logicDelete = tableInfo.getClass().getDeclaredField("logicDelete");
logicDelete.setAccessible(true);
logicDelete.set(tableInfo, false);
catch (NoSuchFieldException e)
e.printStackTrace();
catch (IllegalAccessException e)
e.printStackTrace();
// 生成 SQL
String sql = String.format(dSqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 第 2 个参数必须和 DBaseMapper 的自定义方法名一致
return this.addDeleteMappedStatement(mapperClass, dSqlMethod.getMethod(), sqlSource);
4、 【DSqlMethod】这个类是我们在【DeletePhysically】使用到,模仿官方代码写的枚举类
/**
* @author Lux Sun
* @date 2022/1/14
*/
public enum DSqlMethod
/**
* 删除
*/
DELETE("deletePhysically", "根据 entity 条件删除记录(物理删除)", "<script>\\nDELETE FROM %s %s %s\\n</script>");
private final String method;
private final String desc;
private final String sql;
DSqlMethod(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;
5、【DSqlInjector】SQL注入器,将我们实现的方法逻辑类注入进来
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author Lux Sun
* @date 2022/1/14
*/
@Component
public class DSqlInjector extends DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass)
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new DeletePhysically());
return methodList;
6、最后我们在【DSqlInjector】类上添加 @Component 注解,使得 Spring 能够扫进来
7、测试
- 数据库表
- TestPO.java
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Lux Sun
* @date 2020/9/8
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName(value = "t_test", autoResultMap = true)
public class TestPO
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@TableField(typeHandler = JacksonTypeHandler.class)
private People people;
@TableLogic
@TableField(fill = FieldFill.INSERT, select = false)
private Integer testDel;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class People
private String name;
private Integer age;
- TestDao.java
- 当然这里我是用 Application 类进行对它扫描了,所以没添加注解
import com.chinadaas.platform.dsp.kernel.common.domain.po.api.TestPO;
import com.chinadaas.platform.dsp.kernel.common.mybatisplus.DBaseMapper;
/**
* @author Lux Sun
* @date 2020/6/16
*/
public interface TestDao extends DBaseMapper<TestPO>
- 测试Controller
@PostMapping("/test")
public ResultVO<Object> addTest(@RequestBody TestPO testPO)
testDao.insertWithPrimary(testPO);
return ResultUtil.buildSucc();
- 图1 是我们 Mock 好该数据先,然后图 2 我们模拟导入的业务,那么虽然结果只是改变了0,但是内部执行过程一定是先删除再插入,而不是 UPDATE 操作(如图3)
- 测试结果 - 图1
- 测试结果 - 图2
- 测试结果 - 图3
相关链接
以上是关于MyBatis-Plus - 一篇带你玩转自定义 BaseMapper的主要内容,如果未能解决你的问题,请参考以下文章
MyBatis-Plus - 一篇带你解决自定义 SQL 注入器失效必杀技