☰
【JavaEE】从 MyBatis 到 MyBatis-Plus: 动态 SQL、条件构造器与生产实践
2026/10/28 19:00:04 网站建设 项目流程

通过本文我们将:

  • 学习 MyBatis 的动态 SQL 查询
  • 掌握 MyBatis 在项目中的应用,可以使用 Spring MVC 完成一些基础的功能
  • 学习使用 MyBatis-Plus 完成基础的增删改查
  • 学习使用 MyBatis-Plus 的条件构造器以及自定义 SQL 完成一些复杂的查询

文章目录

  • 1. 动态 SQL:让 SQL 跟着参数 “长出来”
    • 1.1 `<if>`:最基础的条件开关
    • 1.2 `<trim>`:手动控制前后缀与多余符号
    • 1.3 `<where>`:自动处理 WHERE 和开头的 AND
    • 1.4 `<set>`:更新语句的逗号清理器
    • 1.5 `<foreach>`:批量操作的核心
    • 1.6 `<sql>` 与 `<include>`:复用列名
    • 1.7 注解方式 vs XML 方式
  • 2. 从 MyBatis 到 MyBatis-Plus:把单表 CRUD 交给框架
    • 2.1 快速上手
    • 2.2 映射规则与三个核心注解
  • 3. 条件构造器:不写 SQL 也能构造复杂 WHERE
    • 3.1 QueryWrapper 与 UpdateWrapper
    • 3.2 Lambda 版本:用方法引用替代字符串
    • 3.3 自定义 SQL:Wrapper 只管 WHERE,其余自己写
  • 4. 生产环境常用的增强特性
    • 4.1 分页:必须注册插件,否则 LIMIT 不生效
    • 4.2 逻辑删除:用 @TableLogic 让 delete 变成 update
    • 4.3 自动填充:createTime / updateTime 不再手写
    • 4.4 乐观锁:并发更新时防止覆盖
  • 5. MyBatis Generator:代码生成
  • 总结

学完 MyBatis 的基础 CRUD 之后,你会很快遇到两个新问题。

  1. 业务查询的条件往往 “不确定”——用户可能只填了年龄,也可能只填了性别,你不能为每一种组合都写一条 SQL。
  2. 单表的增删改查写多了会变成体力活,每个实体都重复一套几乎一样的代码。

MyBatis 用动态 SQL解决第一个问题,MyBatis-Plus 用增强的 BaseMapper 和条件构造器解决第二个问题。

两者不是替代关系,而是配合关系:单表走 MyBatis-Plus 提效,复杂 SQL 继续手写 XML。

1. 动态 SQL:让 SQL 跟着参数 “长出来”

动态 SQL 的本质是在 XML 中通过标签控制 SQL 片段的拼接,最终发给数据库的仍然是普通的、静态的 SQL 语句。它的目的是避免为每一种参数组合写一条独立的 SQL。

1.1<if>:最基础的条件开关

注册用户时,性别可能是选填的。如果直接写成INSERT INTO ... (gender) VALUES (#{gender}),当gender为null时会把NULL写进去;如果数据库该字段有默认值,反而会覆盖默认值。正确的做法是用<if>把整个字段(包括列名和值)一起包起来:

<insertid="insertUserByCondition">INSERT INTO userinfo ( username, password, age,<iftest="gender != null">gender,</if>phone ) VALUES ( #{username}, #{password}, #{age},<iftest="gender != null">#{gender},</if>#{phone} )</insert>

test中写的是传入对象的属性名,而不是数据库字段名。这是初学者最容易混淆的地方:gender对应的是UserInfo类中的gender属性。

1.2<trim>:手动控制前后缀与多余符号

当可选字段变多时,逐个用<if>判断会变得零散。<trim>可以把一组动态片段 “包” 起来,统一处理前缀、后缀和多余的逗号。以插入为例:

<insertid="insertUserByCondition">INSERT INTO userinfo<trimprefix="("suffix=")"suffixOverrides=","><iftest="username != null">username,</if><iftest="password != null">password,</if><iftest="age != null">age,</if><iftest="gender != null">gender,</if><iftest="phone != null">phone,</if></trim>VALUES<trimprefix="("suffix=")"suffixOverrides=","><iftest="username != null">#{username},</if><iftest="password != null">#{password},</if><iftest="age != null">#{age},</if><iftest="gender != null">#{gender},</if><iftest="phone != null">#{phone},</if></trim></insert>

prefix和suffix分别在整块内容前后加上指定字符;suffixOverrides=","则负责去掉最后一个多余的逗号。<where>和<set>本质上就是预置了常用参数的<trim>。

1.3<where>:自动处理 WHERE 和开头的 AND

条件查询的动态拼接有一个经典陷阱:如果第一个条件前面写了AND,而后续条件都不成立,SQL 就会变成WHERE AND age = ?。<where>的规则是:只有当内部有内容时才插入WHERE,并且自动去掉开头多余的AND或OR:

<selectid="queryByCondition"resultType="UserInfo">SELECT id, username, age, gender, phone FROM userinfo<where><iftest="age != null">AND age = #{age}</if><iftest="gender != null">AND gender = #{gender}</if><iftest="deleteFlag != null">AND delete_flag = #{deleteFlag}</if></where></select>

需要注意:<where>只有在所有条件都不成立时才会整体消失。如果你用<trim prefix="WHERE" prefixOverrides="AND |OR ">来替代,当内部没有任何内容时,where关键字仍然会被保留,这是两者的关键区别。

  • 管道符和尾部空格都是语法的一部分

1.4<set>:更新语句的逗号清理器

更新时常见的错误是SET username = ?,末尾多了一个逗号。<set>会动态插入SET,并删掉最后多余的逗号:

<updateid="updateUserByCondition">UPDATE userinfo<set><iftest="username != null">username = #{username},</if><iftest="age != null">age = #{age},</if><iftest="deleteFlag != null">delete_flag = #{deleteFlag},</if></set>WHERE id = #{id}</update>

如果<set>内部所有条件都不成立,SQL 会变成UPDATE userinfo WHERE id = ?,这是非法语句,因此更新场景通常要保证至少有一个字段会被设置,或者在业务层做前置校验,例如在 Service 层判断username、age、deleteFlag至少一个不为null再调用 Mapper,或改用LambdaUpdateWrapper显式set确定要更新的字段。

1.5<foreach>:批量操作的核心

根据多个 ID 删除,或者批量插入,都依赖<foreach>遍历集合:

<deleteid="deleteByIds">DELETE FROM userinfo WHERE id IN<foreachcollection="ids"item="id"separator=","open="("close=")">#{id}</foreach></delete>

collection写的是方法参数名(当 List 是方法的唯一参数且未加@Param时,collection可以写list或collection;如果方法有多个参数,则必须通过@Param显式指定集合参数的名称。);open和close构成括号;separator是元素之间的分隔符。

1.6<sql>与<include>:复用列名

多个查询语句重复写同一串列名既容易写错,维护时也容易漏改。把公共片段抽出来:

<sqlid="allColumn">id, username, age, gender, phone, delete_flag, create_time, update_time</sql><selectid="queryAllUser"resultType="UserInfo">SELECT<includerefid="allColumn"/>FROM userinfo</select>

<include>通过refid引用片段。这在列名很多、或者多个查询共享相同基础列时尤其有用。

1.7 注解方式 vs XML 方式

动态 SQL 也可以写在注解里,但需要用<script>把整段 SQL 包起来:

@Select("<script>SELECT * FROM userinfo "+"<where>"+"<if test='age != null'>AND age = #{age}</if>"+"</where></script>")List<UserInfo>queryByCondition(UserInfouserInfo);

初学者建议优先使用 XML。注解方式下,IDE 不会对字符串里的标签做格式检查和语法提示,一个引号写错就要排查很久;而 XML 有结构校验和标签高亮,出错更容易发现。复杂 SQL 用 XML,简单语句用注解,这是更稳妥的分工。

2. 从 MyBatis 到 MyBatis-Plus:把单表 CRUD 交给框架

动态 SQL 解决了 “条件不确定” 的问题,但单表的增删改查本身仍然在重复。MyBatis-Plus 在 MyBatis 之上只做增强、不做改变,它把单表 CRUD、分页、逻辑删除等高频操作封装成了开箱即用的能力。

2.1 快速上手

引入对应版本的 Starter。Spring Boot 3 项目需要注意使用专用的 starter(示例版本,请以 Maven Central 最新稳定版为准):

<!-- Spring Boot 2 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.7</version></dependency><!-- Spring Boot 3 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency>

实体类直接对应表字段,Mapper 只需继承BaseMapper<T>,单表 CRUD 就全部具备了:

@MapperpublicinterfaceUserInfoMapperextendsBaseMapper<UserInfo>{}

BaseMapper已经提供了insert、selectById、selectBatchIds、updateById、deleteById等一整套方法,无需编写任何 XML 或注解 SQL。

2.2 映射规则与三个核心注解

MyBatis-Plus 默认按以下规则推断映射关系:表名由实体类驼峰转蛇形(UserInfo→user_info);字段名由属性名驼峰转蛇形(deleteFlag→delete_flag);主键默认识别id。如果命名不符合这个规则,用注解显式声明:

  • @TableName:指定实体类对应的表名,例如@TableName("user_info")。
  • @TableField:指定属性对应的字段名,例如@TableField("delete_flag")。
  • @TableId:指定主键字段。属性名与字段名一致时,单独写@TableId即可;不一致时需要@TableId("id")并标注在对应属性上。

3. 条件构造器:不写 SQL 也能构造复杂 WHERE

BaseMapper的方法大多接受一个Wrapper<T>参数。Wrapper 是条件构造器,它允许你用链式调用的方式描述 WHERE 条件,MyBatis-Plus 负责把它翻译成 SQL。主要分为两族:QueryWrapper / UpdateWrapper使用字符串字段名;LambdaQueryWrapper / LambdaUpdateWrapper使用实体类的方法引用。

3.1 QueryWrapper 与 UpdateWrapper

查询示例:

QueryWrapper<UserInfo>wrapper=newQueryWrapper<UserInfo>().select("id","username","age").eq("age",18).like("username","min");List<UserInfo>users=userInfoMapper.selectList(wrapper);

更新示例,直接在 Wrapper 中设置更新字段,不需要创建实体对象:

UpdateWrapper<UserInfo>updateWrapper=newUpdateWrapper<UserInfo>().set("delete_flag",0).set("age",5).in("id",List.of(1,2,3));userInfoMapper.update(updateWrapper);

如果更新值本身是一个 SQL 表达式(例如age = age + 10),使用setSql:

newUpdateWrapper<UserInfo>().setSql("age = age + 10").in("id",List.of(1,2,3));

常用条件方法:eq(等于)、ne(不等于)、gt/ge/lt/le(大于/大于等于/小于/小于等于)、like、in、between等。

3.2 Lambda 版本:用方法引用替代字符串

QueryWrapper 的字符串字段名有三个隐患:拼写错误编译期无法发现;字段改名后不会自动跟随;部分场景下select指定字段后结果映射可能对不上。LambdaQueryWrapper 通过方法引用解决了前两个问题:

LambdaQueryWrapper<UserInfo>wrapper=newLambdaQueryWrapper<UserInfo>().select(UserInfo::getUsername,UserInfo::getAge).eq(UserInfo::getAge,18);

LambdaUpdateWrapper 同理:

LambdaUpdateWrapper<UserInfo>updateWrapper=newLambdaUpdateWrapper<UserInfo>().set(UserInfo::getDeleteFlag,0).in(UserInfo::getId,List.of(1,2,3));

选择建议:单表操作优先用 Lambda 版本,获得编译期类型安全;多表联查或需要引用其他表的字段时,QueryWrapper 的字符串方式更灵活。

3.3 自定义 SQL:Wrapper 只管 WHERE,其余自己写

Wrapper 并非只能配合 BaseMapper 的内置方法使用。MyBatis-Plus 允许你把 Wrapper 构建的 WHERE 条件片段注入到自定义 SQL 中,SQL 的其余部分完全由你控制。以 “指定 ID 范围扣减余额” 为例:

// 1. 构建条件LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<User>().in(User::getId,List.of(1L,2L,4L));// 2. Mapper 方法:wrapper 参数名必须为 ew@Update("UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}")voidupdateBalanceByIds(@Param("amount")intamount,@Param("ew")LambdaQueryWrapper<User>wrapper);

关键规则有三条:

  1. 参数名必须是ew,或者用@Param(Constants.WRAPPER)显式指定。
  2. SQL 中使用${ew.customSqlSegment}(美元符号,不是#{})来插入 Wrapper 生成的 WHERE 片段。美元符号是字符串直接拼接,而customSqlSegment本身已经包含了安全的参数绑定结构。
  3. 自定义 SQL 时,Wrapper 不会再基于实体类自动生成 WHERE,你需要完整写出条件之外的 SQL 部分。

4. 生产环境常用的增强特性

4.1 分页:必须注册插件,否则 LIMIT 不生效

MyBatis-Plus 的分页依赖拦截器改写 SQL。如果只创建Page对象而没有注册分页插件,分页不会生效,total也会返回 0。配置类中注册:

@ConfigurationpublicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}}

使用时分页参数放在第一位,Mapper 的 XML 中不要再手写LIMIT ,否则会和插件生成的分页子句冲突:

Page<UserInfo>page=newPage<>(1,10);userInfoMapper.selectPage(page,wrapper);

4.2 逻辑删除:用 @TableLogic 让 delete 变成 update

数据库中的数据通常不建议物理删除。在实体类的删除标识字段上加@TableLogic:

@TableLogicprivateIntegerdeleteFlag;

加上注解后,deleteById会自动改写为UPDATE ... SET delete_flag = 1 WHERE id = ?;所有查询也会自动附加delete_flag = 0条件。你不需要在每个查询里手动写delete_flag = 0,框架会在 SQL 改写阶段统一注入。

需要注意,逻辑删除字段如果参与了唯一索引,已删除的数据仍然占用索引位置,再次插入相同业务键会触发唯一冲突,表设计时要提前考虑这一点,常见解法是将delete_flag纳入联合唯一索引,或使用时间戳/主键作为已删除记录的唯一值。

4.3 自动填充:createTime / updateTime 不再手写

创建时间和更新时间是几乎每张表都有的字段。用@TableField(fill = ...)标注填充时机,再实现MetaObjectHandler提供填充值:

@TableField(fill=FieldFill.INSERT)privateDatecreateTime;@TableField(fill=FieldFill.INSERT_UPDATE)privateDateupdateTime;
@ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{@OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,"createTime",Date.class,newDate());this.strictInsertFill(metaObject,"updateTime",Date.class,newDate());}@OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,"updateTime",Date.class,newDate());}}

字段必须声明@TableField并设置fill,否则自动填充不会生效。

4.4 乐观锁:并发更新时防止覆盖

乐观锁通过版本号避免 “后提交的更新覆盖先提交的更新”。在版本字段上加@Version,并注册乐观锁插件:

@VersionprivateIntegerversion;
interceptor.addInnerInterceptor(newOptimisticLockInnerInterceptor());

更新时 MyBatis-Plus 会自动带上WHERE version = 旧值并将版本号加一。如果版本已被其他请求修改,更新影响行数为 0,业务层可以根据这个结果提示用户重试。注意乐观锁插件只对updateById和update(entity, wrapper)生效(在 3.5.7+ 版本中,saveOrUpdate和insertOrUpdate也受乐观锁插件支持)。

5. MyBatis Generator:代码生成

当表数量较多时,手写实体类、Mapper 接口仍然有重复劳动。MyBatis Generator 可以根据数据库表反向生成 Model、Mapper 接口和 XML。generatorConfig.xml中最关键的参数是<context>的targetRuntime,它决定生成代码的风格(以所用 MBG 版本官方文档为准):

targetRuntime生成内容适用场景
MyBatis3DynamicSql(较新版本默认)仅 Java,基于 MyBatis Dynamic SQL 库,无 XML偏好链式 Java 查询、不想维护 XML
MyBatis3(旧版曾默认为 MyBatis3)Java + XML,包含 Example 类传统 XML 风格,需要 Example 条件查询
MyBatis3SimpleJava + XML,只包含基础 CRUD想要干净简洁的生成结果,不需要 Example

配置好jdbcConnection、javaModelGenerator、sqlMapGenerator和javaClientGenerator后,通过 Maven 插件的mybatis-generator:generate目标执行即可。

如果生成后仍需要动态更新逻辑,可以在生成的 Mapper 中追加自定义方法,与 MyBatis-Plus 的 Wrapper 配合使用。

总结

MyBatis 的动态 SQL 解决的是条件不确定的问题:

  1. <if>控制单个字段是否拼接
  2. <trim>/<where>/<set>处理前后缀和多余符号
  3. <foreach>完成集合遍历
  4. <sql>/<include>消除重复列名。

它让你不必为每一种参数组合写一条独立 SQL,但单表 CRUD 的重复代码仍然存在。

MyBatis-Plus 在完全不改变 MyBatis 的前提下,把单表操作抽象为BaseMapper和 Wrapper:Lambda 版本提供编译期类型安全,自定义 SQL 通过${ew.customSqlSegment}让 Wrapper 只负责 WHERE,其余部分继续手写。

分页插件、逻辑删除、自动填充、乐观锁则把生产环境中反复出现的配置问题收拢到框架层面。

MyBatis 和 MyBatis-Plus 两者是配合关系而非替代关系:单表走 MyBatis-Plus,复杂 SQL 继续用 MyBatis 的动态 SQL 手写,这才是实际项目中最顺畅的分工。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询