MyBatis 从入门到精通(第三篇)—— 动态 SQL、关联查询与查询缓存
在前两篇博客中,我们掌握了 MyBatis 的基础搭建、核心架构与 Mapper 代理开发,能应对简单的单表 CRUD 场景。但实际项目中,业务往往更复杂 —— 比如 “多条件动态查询”“员工与部门的关联查询”“高频查询的性能优化” 等。本篇将聚焦 MyBatis 的三大高级特性:动态 SQL(灵活拼接 SQL)、关联查询(处理多表关系)、查询缓存(提升性能),结合文档中的实战案例,帮你解决复杂业务场景,真正做到 “学以致用”。
目录
一、动态 SQL:MyBatis 的 “灵活拼接神器”
1.1 动态 SQL 的核心价值
1.2 核心动态 SQL 标签实战
(1)标签:基础条件判断
(2)标签:智能处理 AND/OR
(3)--标签:二选一的条件
(4)标签:自定义前缀 / 后缀
(5)标签:动态更新的 “逗号杀手”
(6)标签:处理集合与 IN 条件
(7)片段:SQL 代码复用
(8)标签:跨数据库模糊查询
二、关联查询:处理多表关系(一对一 / 一对多 / 多表)
2.1 关联查询的核心概念
2.2 一对一关联查询:员工→部门
实现步骤:
2.3 一对多关联查询:部门→员工
实现步骤:
2.4 多表关联查询:用户→订单→订单详情→商品
实现步骤:
三、查询缓存:MyBatis 的 “性能优化利器”
3.1 缓存的核心概念
3.2 一级缓存:SqlSession 级别的本地缓存
核心特点:
实战案例:
3.3 二级缓存:Mapper 级别的全局缓存
核心特点:
配置步骤:
实战案例:
关键配置:
3.4 整合第三方缓存:Ehcache(分布式场景)
配置步骤:
四、总结:MyBatis 核心能力回顾与实践建议
实践建议:
一、动态 SQL:MyBatis 的 “灵活拼接神器”
实际开发中,SQL 语句往往不是固定的 —— 比如 “查询员工” 时,用户可能输入姓名查询,也可能输入部门号查询,还可能两者都输入。如果为每种情况写一个 SQL,会导致代码冗余。MyBatis 的动态 SQL 通过标签判断条件,自动拼接 SQL,彻底解决这一问题。
1.1 动态 SQL 的核心价值
动态 SQL 的本质是 “根据参数是否为空或满足条件,动态生成合法的 SQL 语句”,避免手动拼接 SQL 的痛点:
- 无需担心 “第一个条件是 AND/OR” 导致的语法错误;
- 无需处理 “字段末尾多余逗号”(如更新操作中);
- 支持循环遍历集合(如
IN
条件),简化批量操作。
MyBatis 提供 8 种常用动态 SQL 标签,我们结合文档中的实战案例,逐一讲解核心用法。
1.2 核心动态 SQL 标签实战
(1)<if>
标签:基础条件判断
<if>
标签是动态 SQL 的基础,用于 “满足条件则拼接 SQL 片段”,常与test
属性(OGNL 表达式)配合使用。
场景:查询员工,姓名不为空则按姓名模糊查询,部门号不为空则按部门号查询。
<select id="selectEmpByCond" parameterType="emp" resultType="emp">select * from emp where 1=1<!-- 若ename不为空且非空字符串,拼接姓名条件 --><if test="ename != null and ename != ''">and ename like concat('%', #{ename}, '%')</if><!-- 若deptno不为空,拼接部门号条件 --><if test="deptno != null">and deptno = #{deptno}</if>
</select>
关键说明:
test="ename != null and ename != ''"
:判断参数ename
是否有效(非空且非空字符串);concat('%', #{ename}, '%')
:MySQL 中拼接模糊查询的%
,避免 SQL 注入(文档中强调#{}
比${}
更安全);where 1=1
:临时占位,避免 “第一个条件是 AND” 导致的语法错误(后续<where>
标签可替代这一写法)。
(2)<where>
标签:智能处理 AND/OR
<where>
标签是<if>
标签的 “好搭档”,能自动处理条件拼接中的语法问题:
- 若内部有满足条件的
<if>
,自动添加WHERE
关键字; - 自动去掉第一个条件前的
AND
或OR
; - 若内部无满足条件的
<if>
,不添加WHERE
,避免语法错误。
优化上述<if>
案例:
<select id="selectEmpByCond" parameterType="emp" resultType="emp">select * from emp<where><if test="ename != null and ename != ''">and ename like concat('%', #{ename}, '%') <!-- 无需担心第一个条件是AND --></if><if test="deptno != null">and deptno = #{deptno}</if></where>
</select>
文档要点:<where>
标签会智能忽略条件开头的AND/OR
,且无需手动写where 1=1
,代码更简洁。
(3)<choose>-<when>-<otherwise>
标签:二选一的条件
类似 Java 的switch-case-default
,<choose>
标签下的<when>
按顺序判断,只执行第一个满足条件的<when>
,都不满足则执行<otherwise>
。
场景:查询员工,优先按薪资查(薪资≤指定值),其次按姓名查,都不满足则查部门号 = 10。
<select id="selectEmpByChoose" parameterType="emp" resultType="emp">select * from emp<where><choose><when test="sal != null">sal <= #{sal} <!-- XML中“<”需转义为“<” --></when><when test="ename != null and ename != ''">ename like concat('%', #{ename}, '%')</when><otherwise>deptno = 10 <!-- 所有条件不满足时执行 --></otherwise></choose></where>
</select>
文档说明:<choose>
适用于 “多个条件中只选一个” 的场景,避免<if>
标签的 “多条件同时生效” 问题。
(4)<trim>
标签:自定义前缀 / 后缀
<trim>
标签比<where>
更灵活,支持自定义 “添加前缀”“添加后缀”“覆盖首尾字符”,核心属性如下:
prefix
:给内部内容添加前缀(如WHERE
);suffix
:给内部内容添加后缀(如)
);prefixOverrides
:去掉内部内容开头的指定字符(如AND/OR
);suffixOverrides
:去掉内部内容末尾的指定字符(如,
)。
场景 1:替代<where>
标签
<trim prefix="where" prefixOverrides="and|or"><if test="ename != null">and ename like '%${ename}%'</if><if test="deptno != null">and deptno = #{deptno}</if>
</trim>
场景 2:动态插入字段(处理末尾逗号)
插入操作中,若部分字段为空,会导致INSERT
语句末尾多逗号,<trim>
可自动去掉:
<insert id="insertEmp" parameterType="emp">insert into emp<!-- 动态拼接字段名,去掉末尾逗号 --><trim prefix="(" suffix=")" suffixOverrides=","><if test="ename != null">ename,</if><if test="job != null">job,</if><if test="sal != null">sal,</if></trim>values<!-- 动态拼接字段值,去掉末尾逗号 --><trim prefix="(" suffix=")" suffixOverrides=","><if test="ename != null">#{ename},</if><if test="job != null">#{job},</if><if test="sal != null">#{sal},</if></trim>
</insert>
文档要点:<trim>
标签是动态 SQL 中最灵活的标签,可应对<where>
和<set>
无法覆盖的场景。
(5)<set>
标签:动态更新的 “逗号杀手”
更新操作中,若用<if>
标签,可能出现 “字段末尾多逗号”(如update emp set ename=?,
),<set>
标签可自动去掉末尾逗号,并添加SET
关键字。
场景:动态更新员工信息,字段不为空则更新该字段。
<update id="updateEmp" parameterType="emp">update emp<set><if test="ename != null">ename = #{ename},</if><if test="job != null">job = #{job},</if><if test="sal != null">sal = #{sal},</if></set>where empno = #{empno}
</update>
文档说明:<set>
标签会自动添加SET
关键字,并去掉内部内容末尾的逗号,避免update
语句语法错误。
(6)<foreach>
标签:处理集合与 IN 条件
当 SQL 需要IN
条件(如where deptno in (10,20,30)
)或批量操作时,<foreach>
标签可遍历集合生成对应 SQL 片段,核心属性如下:
collection
:集合类型(list
=List,array
= 数组,map的key
=Map 中的集合);open
:遍历开始符号(如(
);close
:遍历结束符号(如)
);item
:集合元素的别名(如deptno
);separator
:元素之间的分隔符(如,
)。
场景 1:遍历 List 集合,查询部门号在列表中的员工
<select id="selectEmpByDeptnos" parameterType="java.util.List" resultType="emp">select * from emp<where>deptno in<foreach collection="list" open="(" close=")" item="deptno" separator=",">#{deptno}</foreach></where>
</select>
场景 2:遍历数组,查询员工编号在数组中的员工
<select id="selectEmpByEmpnosArr" parameterType="int[]" resultType="emp">select * from emp<where>empno in<foreach collection="array" open="(" close=")" item="empno" separator=",">#{empno}</foreach></where>
</select>
文档要点:collection
属性需根据参数类型选择(List 用list
,数组用array
),若参数是 Map,需写 Map 中集合的key
。
(7)<sql>
片段:SQL 代码复用
若多个 SQL 有重复片段(如select empno, ename, job from emp
),可提取为<sql>
片段,避免重复书写,提升维护性。
<!-- 定义SQL片段:id为片段唯一标识 -->
<sql id="empColumns">empno, ename, job, sal, deptno
</sql><!-- 引用SQL片段:用<include refid="片段id"> -->
<select id="selectEmp" resultType="emp">select <include refid="empColumns"/> from emp
</select><select id="selectEmpByDeptno" parameterType="int" resultType="emp">select <include refid="empColumns"/> from emp where deptno = #{deptno}
</select>
文档说明:<sql>
片段适用于重复的字段列表、查询条件等,减少代码冗余。
(8)<bind>
标签:跨数据库模糊查询
不同数据库的模糊查询语法不同(MySQL 用concat
,Oracle 用||
),<bind>
标签可定义变量统一语法,提升代码可移植性。
<select id="selectEmpByEname" parameterType="emp" resultType="emp"><!-- 定义变量name:值为“%+ename+%” --><bind name="name" value="'%' + ename + '%'"/>select * from emp where ename like #{name}
</select>
文档要点:<bind>
标签无需关心数据库类型,统一用#{name}
引用变量,避免因数据库切换修改 SQL。
二、关联查询:处理多表关系(一对一 / 一对多 / 多表)
实际业务中,单表查询很少见,更多是 “员工 - 部门”“部门 - 员工”“用户 - 订单 - 商品” 等多表关联场景。MyBatis 通过<resultMap>
标签的<association>
(一对一)和<collection>
(一对多)子标签,实现复杂关联映射。
2.1 关联查询的核心概念
在讲解案例前,需明确两种常见关联关系:
- 一对一:一个对象对应一个对象(如一个员工对应一个部门);
- 一对多:一个对象对应多个对象(如一个部门对应多个员工);
- 多对多:需通过中间表转换为 “一对多 + 多对一”(如用户 - 订单 - 商品,用户与商品是多对多,通过订单表关联)。
MyBatis 通过<resultMap>
定义关联规则,无需手动遍历多表结果集,自动映射为实体类对象。
2.2 一对一关联查询:员工→部门
场景:查询员工信息时,同时查询员工所属的部门信息(一个员工只属于一个部门)。
实现步骤:
定义实体类:在Emp
类中添加Dept
属性,存储关联的部门信息。
public class Emp {private int empno;private String ename;private int deptno;private Dept dept; // 一对一关联:员工所属部门// getter/setter、toString()
}public class Dept {private int deptno;private String dname;private String loc;// getter/setter、toString()
}
编写 Mapper 接口:定义查询方法。
public interface EmpMapper {List<Emp> selectEmpWithDept(); // 查询员工及所属部门
}
配置 Mapper.xml:用<resultMap>
+<association>
定义关联映射。
<mapper namespace="com.jr.mapper.EmpMapper"><!-- 定义resultMap:映射Emp与Dept的一对一关联 --><resultMap id="empWithDeptMap" type="emp"><!-- 映射Emp的基本字段:id标签对应主键 --><id column="empno" property="empno"/><result column="ename" property="ename"/><result column="deptno" property="deptno"/><!-- 一对一关联Dept:用<association> --><association property="dept" javaType="dept"> <!-- javaType:关联实体类的类型(Dept) --><id column="d_deptno" property="deptno"/> <!-- 用别名避免字段名冲突 --><result column="dname" property="dname"/><result column="loc" property="loc"/></association></resultMap><!-- 关联查询SQL:多表连接,用别名区分字段 --><select id="selectEmpWithDept" resultMap="empWithDeptMap">select e.empno, e.ename, e.deptno, d.deptno as d_deptno, d.dname, d.locfrom emp einner join dept d on e.deptno = d.deptno</select>
</mapper>
- 测试代码:
@Test
public void testSelectEmpWithDept() {SqlSession session = factory.openSession();EmpMapper empMapper = session.getMapper(EmpMapper.class);List<Emp> emps = empMapper.selectEmpWithDept();for (Emp emp : emps) {System.out.println("员工:" + emp.getEname() + ",部门:" + emp.getDept().getDname());}session.close();
}
文档要点:<association>
标签用于一对一关联,javaType
属性指定关联实体类的类型,需用别名避免多表字段名冲突(如d.deptno as d_deptno
)。
2.3 一对多关联查询:部门→员工
场景:查询部门信息时,同时查询部门下的所有员工(一个部门有多个员工)。
实现步骤:
定义实体类:在Dept
类中添加List<Emp>
属性,存储关联的员工列表。
public class Dept {private int deptno;private String dname;private String loc;private List<Emp> emps; // 一对多关联:部门下的员工列表// getter/setter、toString()
}
编写 Mapper 接口:
public interface DeptMapper {Dept selectDeptWithEmp(int deptno); // 查询部门及下属员工
}
配置 Mapper.xml:用<resultMap>
+<collection>
定义一对多关联。
<mapper namespace="com.jr.mapper.DeptMapper"><!-- 定义resultMap:映射Dept与Emp的一对多关联 --><resultMap id="deptWithEmpMap" type="dept"><!-- 映射Dept的基本字段 --><id column="deptno" property="deptno"/><result column="dname" property="dname"/><result column="loc" property="loc"/><!-- 一对多关联Emp列表:用<collection> --><collection property="emps" ofType="emp"> <!-- ofType:集合中元素的类型(Emp) --><id column="e_empno" property="empno"/> <!-- 别名避免冲突 --><result column="e_ename" property="ename"/><result column="e_sal" property="sal"/></collection></resultMap><!-- 关联查询SQL:左连接查询部门与员工 --><select id="selectDeptWithEmp" parameterType="int" resultMap="deptWithEmpMap">select d.deptno, d.dname, d.loc,e.empno as e_empno, e.ename as e_ename, e.sal as e_salfrom dept dleft join emp e on d.deptno = e.deptnowhere d.deptno = #{deptno}</select>
</mapper>
- 测试代码:
@Test
public void testSelectDeptWithEmp() {SqlSession session = factory.openSession();DeptMapper deptMapper = session.getMapper(DeptMapper.class);Dept dept = deptMapper.selectDeptWithEmp(10);System.out.println("部门:" + dept.getDname());for (Emp emp : dept.getEmps()) {System.out.println(" 员工:" + emp.getEname() + ",薪资:" + emp.getSal());}session.close();
}
文档要点:<collection>
标签用于一对多关联,ofType
属性指定集合元素的类型(区别于javaType
,javaType
用于指定属性类型,如List
)。
2.4 多表关联查询:用户→订单→订单详情→商品
场景:查询用户信息时,同时查询用户的所有订单、每个订单的详情、每个详情对应的商品(多表关联:用户 1:N 订单 1:N 订单详情 1:1 商品)。
实现步骤:
定义实体类:逐层关联(Users→Orders→OrderDetail→Items)。
// 用户类:1个用户对应多个订单
public class Users {private int uid;private String uname;private List<Orders> orders; // 一对多关联订单// getter/setter
}// 订单类:1个订单对应多个详情
public class Orders {private int oid;private String orderid;private List<OrderDetail> orderdetails; // 一对多关联详情// getter/setter
}// 订单详情类:1个详情对应1个商品
public class OrderDetail {private int odid;private int itemsnum;private Items item; // 一对一关联商品// getter/setter
}// 商品类
public class Items {private int iid;private String name;private double price;// getter/setter
}
配置 Mapper.xml:嵌套<collection>
和<association>
实现多表映射。
<mapper namespace="com.jr.mapper.UserMapper"><!-- 多表关联resultMap:用户→订单→详情→商品 --><resultMap id="userOrderDetailItemMap" type="users"><id column="uid" property="uid"/><result column="uname" property="uname"/><!-- 1:N:用户→订单 --><collection property="orders" ofType="orders"><id column="oid" property="oid"/><result column="orderid" property="orderid"/><!-- 1:N:订单→订单详情 --><collection property="orderdetails" ofType="orderdetail"><id column="odid" property="odid"/><result column="itemsnum" property="itemsnum"/><!-- 1:1:订单详情→商品 --><association property="item" javaType="items"><id column="iid" property="iid"/><result column="name" property="name"/><result column="price" property="price"/></association></collection></collection></resultMap><!-- 多表关联SQL:四表连接 --><select id="selectUserWithAll" resultMap="userOrderDetailItemMap">select u.uid, u.uname,o.oid, o.orderid,od.odid, od.itemsnum,i.iid, i.name, i.pricefrom users uinner join orders o on u.uid = o.useridinner join orderdetail od on o.orderid = od.orderidinner join items i on od.itemid = i.iid</select>
</mapper>
文档要点:多表关联需嵌套使用<collection>
(一对多)和<association>
(一对一),确保每层映射的column
与property
对应。
三、查询缓存:MyBatis 的 “性能优化利器”
缓存是 “以空间换时间” 的优化手段,MyBatis 提供两级缓存,减少数据库访问次数,提升高频查询的性能。
3.1 缓存的核心概念
MyBatis 的缓存分为两级,作用域和生命周期不同:
- 一级缓存:
SqlSession
级别(本地缓存),默认开启,无需配置; - 二级缓存:
Mapper
(namespace)级别(全局缓存),默认关闭,需手动配置; - 第三方缓存:如 Ehcache、Redis,用于分布式场景(多服务共享缓存)。
3.2 一级缓存:SqlSession 级别的本地缓存
核心特点:
- 作用域:同一个
SqlSession
(从openSession()
到close()
); - 实现:基于
PerpetualCache
(HashMap)存储; - 失效场景:
- 调用
SqlSession.close()
; - 调用
SqlSession.commit()
/rollback()
(事务提交 / 回滚会清空缓存); - 执行相同 ID 的
insert
/update
/delete
(修改数据会清空缓存,避免脏读); - 调用
SqlSession.clearCache()
(手动清空缓存)。
- 调用
实战案例:
@Test
public void testFirstLevelCache() {SqlSession session = factory.openSession();EmpMapper empMapper = session.getMapper(EmpMapper.class);// 第一次查询:执行SQL,结果存入一级缓存Emp emp1 = empMapper.selectEmpByNo(7369);System.out.println(emp1);// 第二次查询:同一SqlSession,相同SQL,从缓存获取(不执行SQL)Emp emp2 = empMapper.selectEmpByNo(7369);System.out.println(emp2);session.close();
}
日志输出:仅第一次查询执行 SQL,第二次从缓存获取。
3.3 二级缓存:Mapper 级别的全局缓存
核心特点:
- 作用域:同一个
Mapper
(namespace),跨SqlSession
共享; - 存储:默认存储序列化后的 Java 对象(需实体类实现
Serializable
接口); - 配置步骤:需开启全局开关 + Mapper 单独配置。
配置步骤:
开启全局二级缓存(SqlMapConfig.xml
):
<settings><setting name="cacheEnabled" value="true"/> <!-- 全局开关,默认true可省略 -->
</settings>
在 Mapper.xml 中开启二级缓存:
<mapper namespace="com.jr.mapper.EmpMapper"><!-- 开启二级缓存:默认使用PerpetualCache --><cache/><!-- 或配置缓存参数(如过期时间、最大容量) --><!--<cacheeviction="LRU" // 淘汰策略(LRU:最近最少使用)flushInterval="60000" // 60秒刷新一次缓存size="1024" // 最多缓存1024个对象readOnly="true"/> // 只读模式(返回对象引用,性能高)--><select id="selectEmpByNo" parameterType="int" resultType="emp">select * from emp where empno = #{empno}</select>
</mapper>
实体类实现 Serializable 接口:
public class Emp implements Serializable { // 二级缓存需序列化private static final long serialVersionUID = 1L; // 序列化ID// 字段、getter/setter
}
实战案例:
@Test
public void testSecondLevelCache() {// 第一个SqlSession:查询后关闭,将数据刷入二级缓存SqlSession session1 = factory.openSession();EmpMapper empMapper1 = session1.getMapper(EmpMapper.class);Emp emp1 = empMapper1.selectEmpByNo(7369);System.out.println(emp1);session1.close(); // 关闭SqlSession,一级缓存数据刷入二级缓存// 第二个SqlSession:从二级缓存获取数据(不执行SQL)SqlSession session2 = factory.openSession();EmpMapper empMapper2 = session2.getMapper(EmpMapper.class);Emp emp2 = empMapper2.selectEmpByNo(7369);System.out.println(emp2);session2.close();
}
文档要点:二级缓存需通过session.close()
或session.commit()
将一级缓存数据刷入,否则无法共享。
关键配置:
- 禁用二级缓存:对实时性要求高的查询(如秒杀商品库存),添加
useCache="false"
:
<select id="selectEmpByNo" parameterType="int" resultType="emp" useCache="false">select * from emp where empno = #{empno}
</select>
- 刷新二级缓存:执行
insert
/update
/delete
后,默认清空二级缓存(避免脏读),可通过flushCache="false"
关闭(不推荐):
<update id="updateEmp" parameterType="emp" flushCache="true"> <!-- 默认true,可省略 -->update emp set ename = #{ename} where empno = #{empno}
</update>
3.4 整合第三方缓存:Ehcache(分布式场景)
默认二级缓存是 “本地缓存”,分布式部署时(多台服务器)缓存不共享,需整合分布式缓存框架(如 Ehcache)。
配置步骤:
添加 Maven 依赖:
<!-- MyBatis-Ehcache整合包 -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.0.2</version>
</dependency><!-- Ehcache核心包 -->
<dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache</artifactId><version>2.10.1</version>
</dependency>
添加 Ehcache 配置文件(ehcache.xml):
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"><!-- 缓存数据存储路径(磁盘) --><diskStore path="D:/mybatis-ehcache"/><!-- 默认缓存配置 --><defaultCachemaxElementsInMemory="1000" <!-- 内存最大缓存对象数 -->eternal="false" <!-- 不永久缓存 -->timeToIdleSeconds="120" <!-- 120秒未访问则过期 -->timeToLiveSeconds="120" <!-- 120秒后过期 -->overflowToDisk="true"/> <!-- 内存满时写入磁盘 -->
</ehcache>
在 Mapper.xml 中指定 Ehcache 缓存:
<mapper namespace="com.jr.mapper.EmpMapper"><!-- 启用Ehcache缓存 --><cache type="org.mybatis.caches.ehcache.EhcacheCache"/><!-- SQL语句... -->
</mapper>
文档要点:Ehcache 支持内存 + 磁盘存储,分布式部署时可配置集群,实现缓存共享。
四、总结:MyBatis 核心能力回顾与实践建议
至此,MyBatis 从入门到精通系列三篇博客已全部完成,我们系统覆盖了 MyBatis 的核心能力:
- 基础层:框架概念、环境搭建(普通项目 + Maven);
- 核心层:三层架构、全局配置、Mapper 代理开发;
- 高级层:动态 SQL、关联查询、查询缓存。
实践建议:
- 动态 SQL:优先用
<where>``<set>
标签简化条件拼接,复杂场景用<trim>
,避免手动写where 1=1
; - 关联查询:一对一用
<association>
,一对多用<collection>
,多表关联需注意字段别名冲突; - 缓存优化:一级缓存默认开启,二级缓存按需开启(适合查询多、修改少的场景),分布式项目整合 Ehcache/Redis;
- 开发规范:坚持 “Mapper 代理开发”,SQL 集中在 XML 中,通过
<sql>
片段复用代码,提升维护性。
MyBatis 的核心优势在于 “轻量、灵活、解耦”—— 既保留了 SQL 的灵活性,又简化了数据映射与连接管理。掌握这些核心能力后,你不仅能应对企业级项目的持久层开发,更能在面试中从容应对 MyBatis 的高频考点(如动态 SQL、缓存机制、关联查询)。后续可进一步学习 MyBatis-Plus(MyBatis 的增强工具,简化 CRUD),但建议先夯实 MyBatis 基础,再逐步拓展。