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 注入器失效必杀技

MyBatis-Plus - 一篇带你解决自定义 SQL 注入器失效必杀技

小玩意 - 一篇带你玩转 SpringBoot 钉钉机器人

一篇带你玩转 Elasticsearch 中文分词

[java基础] 内部类@ 一篇带你玩透(超详解)

[ C语言 ]一篇带你了解浮点型在内存中的存储