大数跨境
0
0

MyBatis-Plus 深度指南:从基础到实战,让 DAO 层开发效率起飞

MyBatis-Plus 深度指南:从基础到实战,让 DAO 层开发效率起飞 供应链架构师
2025-11-28
0
导读:MyBatis-Plus 的核心价值在于:用最小的改造成本,实现 DAO 层开发效率的质的飞跃。
架构师(JiaGouX)
我们都是架构师!
架构未来,你来不来?




  • 1、MyBatis-Plus 简介:不止是增强,更是重构
  • 2、核心功能:单表操作的 "全能工具箱"
    • 2.1 基础 CRUD:单表操作 "零代码" 实现
    • 2.2 条件构造器:复杂查询 "优雅编码"
    • 2.3 丰富的插件集合:功能扩展的 "万能接口"
  • 3、使用建议:写出高效、安全的 MP 代码
    • 3.1 优先使用 Lambda 条件构造器
    • 3.2 条件构造器 NULL 值处理:减少冗余代码
    • 3.3 尽量明确 select 字段:提升查询效率
  • 4、开发实战:从 "能用" 到 "用好" 的进阶(重点)
    • 4.1 基础能力:微服务下的查询条件封装
    • 4.2 快速接入:3 步实现单表 CRUD 接口
    • 4.3 丰富 BaseMapper:扩展自定义通用方法
    • 4.4 内置插件:增强系统安全性
  • 5、总结:为什么 MyBatis-Plus 值得用?





1、MyBatis-Plus 简介:不止是增强,更是重构


MyBatis-Plus 可以理解为「MyBatis + 瑞士军刀皮肤 + 防删库保险栓」—— 它在保留 MyBatis 原生特性的基础上,通过 "零侵入" 设计实现了单表操作的极简开发。

其核心价值体现在三方面:

  • 瑞士军刀般的便捷性:将 XML 配置的 "青铜时代" 升级为 Lambda 表达式的 "赛博坦时代",用极简代码实现复杂操作;
  • 保险栓级的安全性:通过拦截器让 "delete from table" 这类危险操作成为不可能,从源头避免删库风险;
  • 零侵入的兼容性:无需修改现有 MyBatis 代码,老项目可平滑迁移,既保留原生 SQL 灵活性,又获得增强功能。





2、核心功能:单表操作的 "全能工具箱"



MyBatis-Plus 的核心功能是围绕单表的 CRUD 展开的,覆盖从基础操作到高级查询的全场景,所有功能均通过 BaseMapper 接口暴露,无需手动实现。

2.1 基础 CRUD:单表操作 "零代码" 实现

BaseMapper 中封装了单表所有基础操作,无需手写 SQL 就能满足 90% 的业务需求。关键方法整理如下:

操作类型 方法示例 作用 注意事项
新增
int insert(T entity)
插入一条记录,返回影响行数
自动忽略 entity 中 null 值的属性
新增 / 更新
boolean insertOrUpdate(T entity)
若记录存在则更新,否则插入
依赖主键查询(建议查主库避免主从延迟)
删除
int deleteById(Serializable id)
根据主键删除单条记录
物理删除,数据恢复需 DBA 协助
删除
int deleteByIds(Collection<?> idList)
批量删除主键对应的记录
需手动控制 idList 大小,防止数据库负载过高
更新
int updateById(T entity)
根据主键更新记录
主键必须非空,忽略 null 值属性(避免 WHERE id=NULL)
查询
T selectById(Serializable id)
根据主键查询单条记录
-
查询
List<T> selectList(Wrapper<T> queryWrapper)
根据条件批量查询
queryWrapper 为 null 时会全表扫描,需谨慎

2.2 条件构造器:复杂查询 "优雅编码"

条件构造器是 MyBatis-Plus 的 "灵魂",支持用面向对象的方式构建 SQL 条件,避免字符串拼接的坑。核心实现有 4 种:

  • QueryWrapper:基础条件构造器,通过字符串指定字段(如 eq("name", "张三"));
  • LambdaQueryWrapper:基于 Lambda 表达式的构造器(如 eq(User::getName, "张三")),推荐优先使用;
  • UpdateWrapper/LambdaUpdateWrapper:用于构建更新条件,支持动态设置 set 值。

为什么优先用 LambdaQueryWrapper?

对比传统写法的优势一目了然:

// ❌ 不推荐:字段拼写错误编译不报错,字段变更易遗漏
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name""张三").gt("age"18);

// ✅ 推荐:编译期检查字段有效性,重构自动更新引用
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getName, "张三").gt(User::getAge, 18);

优势体现在:

  • 防止字段拼写错误导致的 SQL 异常(编译期校验);
  • 自动校验值类型,避免因类型不匹配导致索引失效;
  • 提高代码可读性,字段含义一目了然。

2.3 丰富的插件集合:功能扩展的 "万能接口"

MyBatis-Plus 通过 MybatisPlusInterceptor 实现插件机制,可拦截 SQL 执行过程并增强功能,核心插件如下:

插件名称 作用 关键说明
PaginationInnerInterceptor
自动分页
必须配置,否则会内存分页(不拼接 limit)
BlockAttackInnerInterceptor
防止全表更新 / 删除
拦截不带 WHERE 条件的 update/delete,避免误操作
OptimisticLockerInnerInterceptor
乐观锁
通过版本号字段解决并发更新冲突
TenantLineInnerInterceptor
多租户
自动为 SQL 添加租户 ID 条件,隔离数据
IllegalSQLInnerInterceptor
SQL 性能规范
拦截不符合规范的 SQL(如 SELECT *)





3、使用建议:写出高效、安全的 MP 代码



3.1 优先使用 Lambda 条件构造器

再强调一次:别用字符串拼条件!LambdaQueryWrapper 能在编译期帮你挡住 "字段不存在"、"类型不匹配" 等一堆坑,重构时还能自动更新引用 —— 相当于给代码加了 "自动纠错"buff。

3.2 条件构造器 NULL 值处理:减少冗余代码

当查询条件含 null 值时,传统写法需用 if 判断避免无效条件,MP 支持 "条件性添加",一行代码搞定:

// ❌ 不推荐:大量if判断
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
    wrapper.eq(User::getName, name);
}
if (age != null) {
    wrapper.eq(User::getAge, age);
}

// ✅ 推荐:条件性添加,减少冗余
wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
       .eq(Objects.nonNull(age), User::getAge, age);

优势:减少 if 判断、避免无效查询条件。

3.3 尽量明确 select 字段:提升查询效率

默认情况下,selectList 会查询所有字段(select *),指定查询字段可利用索引覆盖、减少数据传输:

// ❌ 不推荐:全表字段查询,浪费资源
List<User> users1 = userMapper.selectList(lambdaQueryWrapper);

// ✅ 推荐:只查询需要的字段
lambdaQueryWrapper.select(User::getId, User::getName, User::getAge);
List<User> users2 = userMapper.selectList(lambdaQueryWrapper);

优势体现在:

  • 利用索引覆盖,避免回表查询,提升 SQL 效率;
  • 减少数据传输和序列化开销,降低数据库压力;
  • 节省内存,尤其对大表查询效果明显。





4、开发实战:从 "能用" 到 "用好" 的进阶(重点)



在实际项目中,尤其是微服务架构下,需要将dao层作为单独的服务对外提供原子能力,此时必须解决 QueryWrapper 在 RPC 场景的痛点,同时实现高效接入、功能扩展及安全保障。

4.1 基础能力:微服务下的查询条件封装

4.1.1 痛点与解决方案

在微服务中,若将 DAO 层封装为独立服务,直接暴露 QueryWrapper 存在 3 大问题:

  • QueryWrapper 结构复杂,序列化 / 反序列化耗时;
  • 逻辑层需引入 mybatis-plus-core 依赖,易导致 Jar 包冲突;
  • 原子层升级 MP 版本时,所有调用方需同步升级,维护成本高。

解决方案:自定义 QueryCondition 替代 QueryWrapper,作为 RPC 入参,兼顾灵活性与轻量性。

4.1.2 QueryCondition 设计

QueryCondition 整合查询、排序、分页及字段选择能力,结构如下:

  • 查询条件集合(queryFieldList):封装 WHERE 子句的条件,包含字段名、匹配规则(如等于、模糊查询)、拼接方式(AND/OR);
  • 排序字段集合(orderFieldList):定义排序字段及排序方式(ASC/DESC);
  • 返回字段集合(selectFieldList):指定查询结果需返回的字段,避免 select *
  • 分页参数:页码(pageNum)和每页条数(pageSize)。
// 综合查询条件类
public class QueryCondition {
    private List<String> selectFieldList; // 返回字段
    private List<QueryField> queryFieldList; // 查询条件
    private List<OrderField> orderFieldList; // 排序字段
    private int pageNum;  // 页码
    private int pageSize; // 每页条数
}

4.1.3 类型安全的构建工具:QueryConditionBuilder

为简化 QueryCondition 的创建,设计 QueryConditionBuilder 工具类,通过 Lambda 表达式实现类型安全构建:

public class QueryConditionBuilder<T{
    private List<SelectFieldBuilder<T>> selectFieldBuilderList; // 返回字段构建
    private List<QueryFieldBuilder<T>> queryFieldBuilderList; // 查询条件构建
    private List<OrderFieldBuilder<T>> orderFieldBuilderList; // 排序字段构建
}

使用示例

@Test
public void selectByQuery() {
    QueryConditionBuilder<User> builder = QueryConditionBuilder.builder();
    QueryCondition condition = builder
        .select(User::getId, User::getName) // 选择返回字段
        .eq(User::getStatus, 1// 相等查询
        .like(User::getName, "张"// 模糊查询
        .in(User::getType, Arrays.asList(123)) // 集合查询
        .orderByDesc(User::getCreateTime) // 排序
        .pageNum(1// 页码
        .pageSize(5// 每页条数
        .build(); // 构建时校验数据类型,不匹配则抛异常
}

4.1.4 接口与实现设计

  • 接口定义:在 dao-contract 中定义通用接口 BaseDao,对外暴露单表操作的CRUD能力
public interface BaseDao<P extends SerializableT{
    @Master
    insert(T entity, Option... option)// 新增
    @Slave
    List<T> selectListByQuery(QueryCondition queryCondition, Option... option)// 条件查询

    // 其他方法...
}
  • 实现设计:在 dao-service 通过抽象类 AbstractBaseDaoImpl 继承 MyBatis-Plus 的 ServiceImpl,并实现 BaseDao 接口,在抽象类中将自定义的 QueryCondition 转换成 LambdaQueryWrapper 再调用 ServiceImpl 中的方法实现 BaseDao 中对外暴漏的所有方法:
public abstract class AbstractBaseDaoImpl<P extends SerializableTM extends BaseMapper<T>> 
        extends ServiceImpl<MTimplements BaseDao<PT
{
            
            @Override
            public T insert(T entity, Option... option) {
                // 适配自定义配置项
                this.beforeOption(option);
                try {
                    if (Objects.nonNull(entity)) {
                        super.save(entity);
                    }
                    return entity;
                } finally {
                    // 回滚自定义配置项
                    this.afterOption(option);
                }
            }
               
            @Override
            public List<T> selectListByQuery(QueryCondition queryCondition, Option... option) {
                if (Objects.isNull(queryCondition)) {
                    return new ArrayList<>();
                }
                this.beforeOption(option);
                try {
                    final Wrapper<T> queryWrapper = this.queryCondition2QueryWrapper(queryCondition);
                    return super.list(queryWrapper);
                } finally {
                    this.afterOption(option);
                }
            }

            // ...... 其他方法的实现
}

4.1.5 整体架构

采用分层架构实现高内聚低耦合:

kf_scaffold/
├── dao-contract/    # 接口定义层:暴露对外RPC接口
├── dao-service/     # 服务实现层:实现接口,依赖MyBatis-Plus
├── dao-plugin/      # 插件扩展层:自定义插件(如全表拦截)
├── dao-spring-boot-starter/  # 自动配置层:封装Starter,简化接入
└── dao-demo/        # 使用示例层:提供接入示例

4.2 快速接入:3 步实现单表 CRUD 接口

以售后单表(AssOrder)为例,快速搭建对外暴露的 CRUD 服务:

4.2.1 接口层工程(ass-dao-contract)

  1. 引入依赖
<dependency>
    <groupId>com.bj58.zhuanzhuan.kf</groupId>
    <artifactId>dao-contract</artifactId>
</dependency>
  1. 定义实体
// 售后单据
public class AssOrderEntity {
    private Long id; // 售后单ID
    // 其他字段...
}
  1. 定义接口:继承 BaseDao,无需编写方法实现
// 售后单表的 CRUD 接口
public interface IAssOrderDao extends BaseDao<LongAssOrderEntity{
}

4.2.2 服务层工程(ass-dao-service)

  1. 引入依赖
<dependency>
    <groupId>com.bj58.zhuanzhuan.kf</groupId>
    <artifactId>dao-spring-boot-starter</artifactId>
</dependency>
  1. 配置数据源:区分主从库,实现读写分离
kf:
  dao:
    data-source:
      master:
        url: jdbc:mysql://localhost:3306/master_db
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://localhost:3306/slave_db
        driver-class-name: com.mysql.cj.jdbc.Driver
  1. 编写实现:继承 AbstractBaseDaoImpl,无需手动实现方法
// 售后单表的 CRUD 接口实现
public class AssOrderDao extends AbstractBaseDaoImpl<LongAssOrderEntityAssOrderMapper
        implements IAssOrderDao 
{
}

效果:通过上述步骤,无需编写 SQL,即可对外提供 AssOrder 表的 CRUD 接口,支持通过 QueryCondition 进行条件查询、排序、分页等操作。

4.3 丰富 BaseMapper:扩展自定义通用方法

BaseMapper 的默认方法若不满足需求(如按实体属性统计数量),可按以下步骤扩展:

4.3.1 自定义 Mapper 接口

定义 MyMapper 继承 BaseMapper,添加自定义方法:

public interface MyMapper<Textends BaseMapper<T{
    // 按实体属性拼接AND条件统计数量
    int countByEntity(T entity);
}

4.3.2 注入方法实现

通过 AbstractMethod 构建 SQL 模板,实现 countByEntity 的逻辑:

public class CountByEntityMethod extends AbstractMethod {
    private static final String SQL_TEMPLATE = "<script>%s SELECT COUNT(%s) FROM %s %s %s\n</script>";
    
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql = String.format(SQL_TEMPLATE, 
            sqlFirst(),                // 前置SQL
            selectColumns(tableInfo, true),  // 计数字段
            tableInfo.getTableName(),  // 表名
            sqlWhereEntityWrapper(true, tableInfo),  // WHERE条件(基于实体属性)
            sqlComment());             // 注释
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return addSelectMappedStatementForOther(mapperClass, "countByEntity", sqlSource, Integer.class);
    }
}

4.3.3 注册自定义方法

通过 SqlInjector 将自定义方法注入 MyBatis-Plus:

// 自定义注入器
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        List<AbstractMethod> methods = super.getMethodList(mapperClass, tableInfo);
        methods.add(new CountByEntityMethod()); // 添加自定义方法
        return methods;
    }
}

// 配置注入器,将自定义注入器放入spring环境中
@Configuration
publicclass MybatisPlusConfig {
    @Bean
    public MySqlInjector customSqlInjector() {
        return new MySqlInjector();
    }
}

4.3.4 使用扩展方法

// 定义 UserMapper 继承自定义的 MyMapper
@Mapper
public interface UserMapper extends MyMapper<User{
}

// 调用示例
@Test
public void testCountByEntity() {
    User user = new User();
    user.setName("张三"); // 按姓名统计
    int count = userMapper.countByEntity(user); 
    System.out.println("符合条件的用户数:" + count);
}

4.4 内置插件:增强系统安全性

为避免生产环境中的误操作,在dao-plugin工程中定义了两个插件:

4.4.1 全表扫描拦截(FullTableScanInterceptor)

  • 功能:拦截无查询条件的 SQL(如 select * from user),防止全表扫描导致的性能问题;
  • 场景:当 QueryCondition 未设置查询条件时,自动拦截并抛异常。

4.4.2 全表更新拦截(BlockFullTableOperationInterceptor)

  • 功能:拦截无更新条件的 SQL(如 update user set status=0),防止全表更新;
  • 价值:避免因条件构造器错误导致的批量数据修改,从源头降低风险。





5、总结:为什么 MyBatis-Plus 值得用?



MyBatis-Plus 的核心价值在于:用最小的改造成本,实现 DAO 层开发效率的质的飞跃

  • 对开发者:减少 90% 的 CRUD 代码,用 Lambda 替代字符串拼接,从 "写 SQL" 转向 "拼条件";
  • 对系统:通过插件机制增强安全性(防删库、SQL 规范)和可扩展性(分页、多租户);
  • 对团队:降低新人上手成本,统一 DAO 层编码规范,减少因 SQL 问题导致的线上故障。

如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享

因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享

·END·

相关阅读:


作者:孟建国,转转履约中台研发工程师,主要负责售后业务

来源:转转技术

版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

架构师

我们都是架构师!



关注架构师(JiaGouX),添加“星标”

获取每天技术干货,一起成为牛逼架构师

技术群请加若飞:1321113940 进架构师群

投稿、合作、版权等邮箱:admin@137x.com

【声明】内容源于网络
0
0
供应链架构师
各类跨境出海行业相关资讯
内容 1847
粉丝 0
供应链架构师 各类跨境出海行业相关资讯
总阅读1.4k
粉丝0
内容1.8k