MyBatis / MyBatis-Plus 笔记
1. MyBatis 概述
1.1 ORM 概念
ORM(Object Relational Mapping,对象关系映射)是一种将关系型数据库中的表结构映射为内存中对象的技术。通过 ORM,开发者可以使用面向对象的方式操作数据库,而不需要直接编写 SQL 语句。
| ORM 框架 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| MyBatis | 半自动化,SQL 由开发者编写 | 灵活、SQL 可优化、学习成本低 | 需要手写 SQL、无 Hibernate 透明 |
| Hibernate | 全自动化,自动生成 SQL | 开发效率高、跨数据库 | SQL 难以优化、复杂查询性能差 |
| JPA | Java 标准规范,Hibernate 是实现 | 标准化、与 Spring Data 集成好 | 复杂场景不如 MyBatis 灵活 |
| Spring JDBC Template | JDBC 封装 | 轻量、简单 | 功能有限 |
1.2 MyBatis 与 Hibernate 对比
| 对比维度 | MyBatis | Hibernate |
|---|---|---|
| 自动化程度 | 半自动(手写 SQL) | 全自动(自动生成 SQL) |
| SQL 优化 | 完全可控 | 需要了解 HQL 生成规则 |
| 学习曲线 | 低 | 高 |
| 性能调优 | 直接优化 SQL | 需调整 Hibernate 配置 |
| 复杂查询 | 灵活 | 较复杂(需 JPQL/Criteria) |
| 数据库依赖 | 较强 | 较弱(跨数据库友好) |
| 缓存 | 一级/二级缓存 | 一级/二级/查询缓存 |
| 主流使用地区 | 中国、日本 | 欧美 |
1.3 为什么国内主流使用 MyBatis
- SQL 可控性强 — 国内互联网公司对 SQL 性能要求极高,DBA 可以直接审核和优化 SQL
- 复杂查询场景多 — 互联网业务报表、统计、多表关联查询频繁,MyBatis 更灵活
- 学习成本低 — 只要求熟悉 SQL,不要求深入理解 ORM 原理
- 历史原因 — 早年 Hibernate 版本配置繁琐,MyBatis(前身 iBatis)更轻量
- 与 Spring Boot 集成成熟 —
mybatis-spring-boot-starter使用广泛
2. MyBatis 核心配置
2.1 mybatis-config.xml 配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局配置 -->
<settings>
<!-- 开启驼峰命名映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- SQL 日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<!-- Mapper 映射文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<package name="com.example.mapper"/>
</mappers>
</configuration><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局配置 -->
<settings>
<!-- 开启驼峰命名映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- SQL 日志 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<!-- Mapper 映射文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<package name="com.example.mapper"/>
</mappers>
</configuration>2.2 Spring Boot 集成
在 Spring Boot 中使用 MyBatis 非常简单,添加 mybatis-spring-boot-starter 依赖即可。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency><dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>2.3 application.yml 配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# mapper XML 文件位置
mapper-locations: classpath:mapper/**/*.xml
# 实体类包名(自动别名)
type-aliases-package: com.example.entity
configuration:
# 驼峰命名映射
map-underscore-to-camel-case: true
# 二级缓存
cache-enabled: true
# SQL 执行超时
default-statement-timeout: 30
# 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 打印 SQL(另一种方式)
logging:
level:
com.example.mapper: debugspring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# mapper XML 文件位置
mapper-locations: classpath:mapper/**/*.xml
# 实体类包名(自动别名)
type-aliases-package: com.example.entity
configuration:
# 驼峰命名映射
map-underscore-to-camel-case: true
# 二级缓存
cache-enabled: true
# SQL 执行超时
default-statement-timeout: 30
# 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 打印 SQL(另一种方式)
logging:
level:
com.example.mapper: debug2.4 启动类与 Mapper 扫描
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描 Mapper 接口
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描 Mapper 接口
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}也可以在单个 Mapper 上使用 @Mapper 注解,但推荐统一使用 @MapperScan。
3. XML 映射
3.1 Mapper 接口与 XML namespace 绑定
Mapper 接口的全限定名必须与 XML 中 <mapper> 的 namespace 属性一致。
package com.example.mapper;
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
}package com.example.mapper;
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 必须等于接口全限定名 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基本查询 -->
<select id="selectById" parameterType="long" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT * FROM user
</select>
<insert id="insert" parameterType="User">
INSERT INTO user(name, email, age) VALUES(#{name}, #{email}, #{age})
</insert>
<update id="update" parameterType="User">
UPDATE user SET name = #{name}, email = #{email}, age = #{age} WHERE id = #{id}
</update>
<delete id="deleteById" parameterType="long">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 必须等于接口全限定名 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基本查询 -->
<select id="selectById" parameterType="long" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT * FROM user
</select>
<insert id="insert" parameterType="User">
INSERT INTO user(name, email, age) VALUES(#{name}, #{email}, #{age})
</insert>
<update id="update" parameterType="User">
UPDATE user SET name = #{name}, email = #{email}, age = #{age} WHERE id = #{id}
</update>
<delete id="deleteById" parameterType="long">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>3.2 标签详解
<select> 标签
<select id="selectByCondition"
parameterType="map"
resultType="User"
resultMap="userResultMap"
flushCache="false"
useCache="true"
timeout="10">
SELECT * FROM user WHERE name = #{name} AND age > #{minAge}
</select><select id="selectByCondition"
parameterType="map"
resultType="User"
resultMap="userResultMap"
flushCache="false"
useCache="true"
timeout="10">
SELECT * FROM user WHERE name = #{name} AND age > #{minAge}
</select>| 属性 | 说明 |
|---|---|
id | 对应 Mapper 接口方法名 |
parameterType | 参数类型(可选,MyBatis 可推断) |
resultType | 返回类型(全限定名或别名) |
resultMap | 引用 <resultMap> 定义 |
flushCache | 是否清空缓存 |
useCache | 是否使用二级缓存 |
timeout | 超时秒数 |
<insert> 标签
<!-- 返回自增主键 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO user(name, email, age) VALUES(#{name}, #{email}, #{age})
</insert>
<!-- Oracle 序列生成主键 -->
<insert id="insertOracle">
<selectKey keyProperty="id" order="BEFORE" resultType="long">
SELECT SEQ_USER.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO user(id, name, email, age) VALUES(#{id}, #{name}, #{email}, #{age})
</insert><!-- 返回自增主键 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO user(name, email, age) VALUES(#{name}, #{email}, #{age})
</insert>
<!-- Oracle 序列生成主键 -->
<insert id="insertOracle">
<selectKey keyProperty="id" order="BEFORE" resultType="long">
SELECT SEQ_USER.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO user(id, name, email, age) VALUES(#{id}, #{name}, #{email}, #{age})
</insert><update> / <delete> 标签
<update id="updateById">
UPDATE user SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteBatch">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete><update id="updateById">
UPDATE user SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteBatch">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>3.3 参数传递(#{} vs ${}、@Param)
#{} 与 ${} 的区别:
| 特性 | #{} | ${} |
|---|---|---|
| 预编译 | 是(使用 PreparedStatement) | 否(直接拼接字符串) |
| SQL 注入 | 安全 | 有风险 |
| 适用场景 | 传值(字段值) | 动态表名、列名、排序字段 |
| 性能 | 高(可缓存执行计划) | 低 |
public interface UserMapper {
// #{} 传值,安全
User selectByName(@Param("name") String name);
// ${} 动态排序,注意防注入
List<User> selectAllOrderBy(@Param("orderColumn") String orderColumn);
// 多个参数必须用 @Param
List<User> selectByCondition(@Param("name") String name,
@Param("minAge") Integer minAge);
}public interface UserMapper {
// #{} 传值,安全
User selectByName(@Param("name") String name);
// ${} 动态排序,注意防注入
List<User> selectAllOrderBy(@Param("orderColumn") String orderColumn);
// 多个参数必须用 @Param
List<User> selectByCondition(@Param("name") String name,
@Param("minAge") Integer minAge);
}<select id="selectByName" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
<select id="selectAllOrderBy" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ASC
</select>
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age >= #{minAge}
</select><select id="selectByName" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
<select id="selectAllOrderBy" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ASC
</select>
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age >= #{minAge}
</select>@Param 注解详解:
// 不使用时,MyBatis 会自动命名(arg0, arg1 或 param1, param2)
// 建议始终使用 @Param 明确命名
List<User> findByNameAndAge(@Param("name") String name,
@Param("age") Integer age);
// POJO 作为参数(不需要 @Param)
List<User> findByCondition(UserQuery query);
// 集合参数
List<User> findByIds(@Param("ids") List<Long> ids);
// Map 参数
List<User> findByMap(Map<String, Object> params);// 不使用时,MyBatis 会自动命名(arg0, arg1 或 param1, param2)
// 建议始终使用 @Param 明确命名
List<User> findByNameAndAge(@Param("name") String name,
@Param("age") Integer age);
// POJO 作为参数(不需要 @Param)
List<User> findByCondition(UserQuery query);
// 集合参数
List<User> findByIds(@Param("ids") List<Long> ids);
// Map 参数
List<User> findByMap(Map<String, Object> params);3.4 结果映射(<resultMap>)
<resultMap id="userResultMap" type="User">
<!-- 主键映射 -->
<id property="id" column="id"/>
<!-- 基本字段映射 -->
<result property="name" column="user_name"/>
<result property="email" column="email"/>
<result property="age" column="age"/>
<!-- 自动映射其他字段(推荐开启驼峰) -->
</resultMap>
<select id="selectAll" resultMap="userResultMap">
SELECT id, user_name, email, age FROM user
</select><resultMap id="userResultMap" type="User">
<!-- 主键映射 -->
<id property="id" column="id"/>
<!-- 基本字段映射 -->
<result property="name" column="user_name"/>
<result property="email" column="email"/>
<result property="age" column="age"/>
<!-- 自动映射其他字段(推荐开启驼峰) -->
</resultMap>
<select id="selectAll" resultMap="userResultMap">
SELECT id, user_name, email, age FROM user
</select>resultMap 的继承:
<resultMap id="baseUserMap" type="User">
<id property="id" column="id"/>
<result property="name" column="user_name"/>
<result property="email" column="email"/>
</resultMap>
<!-- 继承 baseUserMap -->
<resultMap id="userDetailMap" extends="baseUserMap" type="User">
<result property="age" column="age"/>
<result property="phone" column="phone"/>
</resultMap><resultMap id="baseUserMap" type="User">
<id property="id" column="id"/>
<result property="name" column="user_name"/>
<result property="email" column="email"/>
</resultMap>
<!-- 继承 baseUserMap -->
<resultMap id="userDetailMap" extends="baseUserMap" type="User">
<result property="age" column="age"/>
<result property="phone" column="phone"/>
</resultMap><association> 一对一关联
public class User {
private Long id;
private String name;
private UserProfile profile; // 一对一关联
// getters/setters
}
public class UserProfile {
private Long id;
private String avatar;
private String phone;
// getters/setters
}public class User {
private Long id;
private String name;
private UserProfile profile; // 一对一关联
// getters/setters
}
public class UserProfile {
private Long id;
private String avatar;
private String phone;
// getters/setters
}<resultMap id="userWithProfileMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 方式一:嵌套查询(N+1 问题,慎用) -->
<association property="profile" column="id"
select="com.example.mapper.ProfileMapper.findByUserId"/>
<!-- 方式二:嵌套结果(推荐,一次查询) -->
<association property="profile" javaType="UserProfile">
<id property="id" column="profile_id"/>
<result property="avatar" column="avatar"/>
<result property="phone" column="phone"/>
</association>
</resultMap>
<select id="selectUserWithProfile" resultMap="userWithProfileMap">
SELECT u.*, p.id as profile_id, p.avatar, p.phone
FROM user u
LEFT JOIN user_profile p ON u.id = p.user_id
WHERE u.id = #{id}
</select><resultMap id="userWithProfileMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 方式一:嵌套查询(N+1 问题,慎用) -->
<association property="profile" column="id"
select="com.example.mapper.ProfileMapper.findByUserId"/>
<!-- 方式二:嵌套结果(推荐,一次查询) -->
<association property="profile" javaType="UserProfile">
<id property="id" column="profile_id"/>
<result property="avatar" column="avatar"/>
<result property="phone" column="phone"/>
</association>
</resultMap>
<select id="selectUserWithProfile" resultMap="userWithProfileMap">
SELECT u.*, p.id as profile_id, p.avatar, p.phone
FROM user u
LEFT JOIN user_profile p ON u.id = p.user_id
WHERE u.id = #{id}
</select><collection> 一对多关联
public class User {
private Long id;
private String name;
private List<Order> orders; // 一对多关联
// getters/setters
}
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
// getters/setters
}public class User {
private Long id;
private String name;
private List<Order> orders; // 一对多关联
// getters/setters
}
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
// getters/setters
}<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 嵌套结果,一对多 -->
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = #{id}
</select><resultMap id="userWithOrdersMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 嵌套结果,一对多 -->
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = #{id}
</select><collection> 高级用法(延迟加载):
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 延迟加载订单列表 -->
<collection property="orders"
column="id"
select="com.example.mapper.OrderMapper.findByUserId"
fetchType="lazy"/>
</resultMap>
<!-- 全局配置延迟加载 -->
<!-- mybatis.configuration.lazy-loading-enabled=true --><resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 延迟加载订单列表 -->
<collection property="orders"
column="id"
select="com.example.mapper.OrderMapper.findByUserId"
fetchType="lazy"/>
</resultMap>
<!-- 全局配置延迟加载 -->
<!-- mybatis.configuration.lazy-loading-enabled=true -->4. 动态 SQL(重点)
动态 SQL 是 MyBatis 最核心的功能之一,可以在 XML 中根据条件动态构建 SQL 语句。
4.1 <if> 标签
<select id="findByCondition" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select><select id="findByCondition" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>注意:XML 中使用 < 和 > 需要转义,或者使用 CDATA:
<if test="age != null">
AND age <![CDATA[ < ]]>= #{age}
</if><if test="age != null">
AND age <![CDATA[ < ]]>= #{age}
</if>4.2 <choose> / <when> / <otherwise> 标签
类似 Java 的 switch-case,只选取第一个匹配的条件。
<select id="findByChoose" resultType="User">
SELECT * FROM user WHERE status = 1
<choose>
<when test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
</otherwise>
</choose>
</select><select id="findByChoose" resultType="User">
SELECT * FROM user WHERE status = 1
<choose>
<when test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
</otherwise>
</choose>
</select>4.3 <where> 标签
<where> 标签自动处理 WHERE 关键字和多余的 AND/OR。
<select id="findByWhere" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
ORDER BY id DESC
</select><select id="findByWhere" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
ORDER BY id DESC
</select><where> 等价于:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<!-- 动态条件 -->
</trim><trim prefix="WHERE" prefixOverrides="AND |OR ">
<!-- 动态条件 -->
</trim>4.4 <set> 标签
<set> 标签用于 UPDATE 语句,动态生成 SET 子句并去除末尾逗号。
<update id="updateBySet">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if>
<if test="phone != null">phone = #{phone},</if>
<!-- update_time 始终更新 -->
update_time = NOW(),
</set>
WHERE id = #{id}
</update><update id="updateBySet">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if>
<if test="phone != null">phone = #{phone},</if>
<!-- update_time 始终更新 -->
update_time = NOW(),
</set>
WHERE id = #{id}
</update><set> 等价于:
<trim prefix="SET" suffixOverrides=",">
<!-- 更新字段 -->
</trim><trim prefix="SET" suffixOverrides=",">
<!-- 更新字段 -->
</trim>4.5 <foreach> 标签
<foreach> 用于遍历集合(List、Set、数组、Map),常用于 IN 查询和批量操作。
<!-- IN 查询 -->
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" index="index"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(name, email, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.age})
</foreach>
</insert>
<!-- Map 参数遍历 -->
<select id="findByMap" resultType="User">
SELECT * FROM user WHERE
<foreach collection="params" index="key" item="value" separator="AND">
${key} = #{value}
</foreach>
</select>
<!-- 批量更新(MySQL 语法) -->
<update id="batchUpdate">
<foreach collection="list" item="user" separator=";">
UPDATE user SET name = #{user.name}, email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update><!-- IN 查询 -->
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" index="index"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(name, email, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.age})
</foreach>
</insert>
<!-- Map 参数遍历 -->
<select id="findByMap" resultType="User">
SELECT * FROM user WHERE
<foreach collection="params" index="key" item="value" separator="AND">
${key} = #{value}
</foreach>
</select>
<!-- 批量更新(MySQL 语法) -->
<update id="batchUpdate">
<foreach collection="list" item="user" separator=";">
UPDATE user SET name = #{user.name}, email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update><foreach> 属性说明:
| 属性 | 说明 | 是否必填 |
|---|---|---|
collection | 集合参数名 | 是 |
item | 元素别名 | 是 |
index | 索引(List 为索引,Map 为 key) | 否 |
open | 开始字符 | 否 |
close | 结束字符 | 否 |
separator | 分隔符 | 否 |
4.6 <trim> 标签
<trim> 是最灵活的标签,可以自定义 SQL 片段的前缀、后缀和覆盖规则。
<!-- 自定义 WHERE 行为 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
<!-- 自定义 SET 行为 -->
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
<!-- 使用 suffix 和 suffixOverrides -->
<insert id="batchInsertWithTrim">
INSERT INTO user(name, email, age)
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<foreach collection="list" item="user">
(#{user.name}, #{user.email}, #{user.age}),
</foreach>
</trim>
</insert><!-- 自定义 WHERE 行为 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
<!-- 自定义 SET 行为 -->
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
<!-- 使用 suffix 和 suffixOverrides -->
<insert id="batchInsertWithTrim">
INSERT INTO user(name, email, age)
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<foreach collection="list" item="user">
(#{user.name}, #{user.email}, #{user.age}),
</foreach>
</trim>
</insert>| 属性 | 说明 |
|---|---|
prefix | SQL 片段前添加的内容 |
suffix | SQL 片段后添加的内容 |
prefixOverrides | 忽略前缀中的匹配字符 |
suffixOverrides | 忽略后缀中的匹配字符 |
4.7 <sql> 与 <include> 标签
复用 SQL 片段的最佳实践。
<!-- 定义 SQL 片段 -->
<sql id="baseColumns">
id, name, email, age, create_time, update_time
</sql>
<sql id="userColumns">
<include refid="baseColumns"/>
, phone, avatar
</sql>
<sql id="whereCondition">
<where>
<if test="name != null">AND name = #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</sql>
<!-- 引用 SQL 片段 -->
<select id="selectById" resultType="User">
SELECT <include refid="baseColumns"/> FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT <include refid="userColumns"/> FROM user
</select>
<select id="findByCondition" resultType="User">
SELECT <include refid="baseColumns"/> FROM user
<include refid="whereCondition"/>
</select><!-- 定义 SQL 片段 -->
<sql id="baseColumns">
id, name, email, age, create_time, update_time
</sql>
<sql id="userColumns">
<include refid="baseColumns"/>
, phone, avatar
</sql>
<sql id="whereCondition">
<where>
<if test="name != null">AND name = #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</sql>
<!-- 引用 SQL 片段 -->
<select id="selectById" resultType="User">
SELECT <include refid="baseColumns"/> FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT <include refid="userColumns"/> FROM user
</select>
<select id="findByCondition" resultType="User">
SELECT <include refid="baseColumns"/> FROM user
<include refid="whereCondition"/>
</select>4.8 <bind> 标签
<bind> 用于在 XML 中创建变量,常用于模糊查询、字符串拼接。
<select id="findByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user WHERE name LIKE #{pattern}
</select>
<!-- 多数据库兼容的模糊查询 -->
<select id="search" resultType="User">
<bind name="likePattern" value="'%' + keyword + '%'"/>
SELECT * FROM user
<where>
<if test="keyword != null">
AND (name LIKE #{likePattern} OR email LIKE #{likePattern})
</if>
</where>
</select>
<!-- 复杂转换 -->
<select id="findByDateRange" resultType="User">
<bind name="startDate" value="start + ' 00:00:00'"/>
<bind name="endDate" value="end + ' 23:59:59'"/>
SELECT * FROM user
WHERE create_time BETWEEN #{startDate} AND #{endDate}
</select><select id="findByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user WHERE name LIKE #{pattern}
</select>
<!-- 多数据库兼容的模糊查询 -->
<select id="search" resultType="User">
<bind name="likePattern" value="'%' + keyword + '%'"/>
SELECT * FROM user
<where>
<if test="keyword != null">
AND (name LIKE #{likePattern} OR email LIKE #{likePattern})
</if>
</where>
</select>
<!-- 复杂转换 -->
<select id="findByDateRange" resultType="User">
<bind name="startDate" value="start + ' 00:00:00'"/>
<bind name="endDate" value="end + ' 23:59:59'"/>
SELECT * FROM user
WHERE create_time BETWEEN #{startDate} AND #{endDate}
</select>4.9 多数据库支持(_databaseId)
<insert id="insertUser" databaseId="mysql">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
<insert id="insertUser" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE" resultType="long">
SELECT SEQ_USER.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO user(id, name, email) VALUES(#{id}, #{name}, #{email})
</insert><insert id="insertUser" databaseId="mysql">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
<insert id="insertUser" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE" resultType="long">
SELECT SEQ_USER.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO user(id, name, email) VALUES(#{id}, #{name}, #{email})
</insert>在 mybatis-config.xml 中配置:
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="PostgreSQL" value="postgresql"/>
</databaseIdProvider><databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="PostgreSQL" value="postgresql"/>
</databaseIdProvider>5. 缓存
5.1 一级缓存(SqlSession 级别)
一级缓存是 MyBatis 默认开启的缓存,作用域为同一个 SqlSession。
工作原理:
- 同一个 SqlSession 中执行相同查询
- MyBatis 会先检查缓存中是否存在结果
- 如果存在则直接返回缓存结果,不再查询数据库
- 如果执行了 INSERT / UPDATE / DELETE / commit / close / clearCache 等操作,缓存会被清空
@Test
public void testL1Cache() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,发 SQL
User u1 = mapper.selectById(1L);
System.out.println(u1);
// 第二次查询,命中一级缓存(不发 SQL)
User u2 = mapper.selectById(1L);
System.out.println(u2);
// u1 == u2 为 true(同一对象引用)
System.out.println(u1 == u2); // true
// 执行更新操作,缓存被清空
mapper.updateName(1L, "NewName");
session.commit(); // 一级缓存被清空
// 第三次查询,缓存已清空,重新发 SQL
User u3 = mapper.selectById(1L);
}
}@Test
public void testL1Cache() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,发 SQL
User u1 = mapper.selectById(1L);
System.out.println(u1);
// 第二次查询,命中一级缓存(不发 SQL)
User u2 = mapper.selectById(1L);
System.out.println(u2);
// u1 == u2 为 true(同一对象引用)
System.out.println(u1 == u2); // true
// 执行更新操作,缓存被清空
mapper.updateName(1L, "NewName");
session.commit(); // 一级缓存被清空
// 第三次查询,缓存已清空,重新发 SQL
User u3 = mapper.selectById(1L);
}
}一级缓存失效场景:
- 不同的 SqlSession
- 手动调用
clearCache() - 执行了 DML 操作(INSERT / UPDATE / DELETE)
- 调用了
commit()/close()/rollback() - 查询参数不同
5.2 二级缓存(namespace 级别)
二级缓存是跨 SqlSession 的,作用域为 Mapper 的 namespace。
开启方式:
<!-- mybatis-config.xml 全局开启缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- Mapper XML 中启用二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基本缓存配置 -->
<cache/>
<!-- 详细缓存配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper><!-- mybatis-config.xml 全局开启缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- Mapper XML 中启用二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基本缓存配置 -->
<cache/>
<!-- 详细缓存配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper><cache> 属性说明:
| 属性 | 说明 | 默认值 |
|---|---|---|
eviction | 缓存回收策略(LRU/FIFO/SOFT/WEAK) | LRU |
flushInterval | 刷新间隔(毫秒) | 不刷新 |
size | 缓存对象个数 | 1024 |
readOnly | 是否只读 | false |
blocking | 是否阻塞 | false |
缓存回收策略:
| 策略 | 说明 |
|---|---|
LRU | 最近最少使用(默认) |
FIFO | 先进先出 |
SOFT | 软引用(基于 GC 回收) |
WEAK | 弱引用(基于 GC 回收) |
实体类实现 Serializable:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// ...
}public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// ...
}二级缓存使用示例:
@Test
public void testL2Cache() {
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
// 第一次查询,缓存到二级缓存
User u1 = mapper1.selectById(1L);
session1.commit(); // 注意:必须提交才会写入二级缓存
// 第二次查询,使用二级缓存(不同 session,不发 SQL)
User u2 = mapper2.selectById(1L);
System.out.println(u1 == u2); // false(反序列化创建的新对象)
} finally {
session1.close();
session2.close();
}
}@Test
public void testL2Cache() {
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
// 第一次查询,缓存到二级缓存
User u1 = mapper1.selectById(1L);
session1.commit(); // 注意:必须提交才会写入二级缓存
// 第二次查询,使用二级缓存(不同 session,不发 SQL)
User u2 = mapper2.selectById(1L);
System.out.println(u1 == u2); // false(反序列化创建的新对象)
} finally {
session1.close();
session2.close();
}
}二级缓存注意事项:
- 需要实体类实现
Serializable - 查询结果需要在
SqlSession.commit()或close()后才会写入二级缓存 - 所有 DML 操作都会清空该 namespace 的二级缓存
- 多表关联查询时,如果其他 Mapper 更新了关联表的数据,缓存不会失效(脏数据问题)
- 分布式环境下需要使用分布式缓存(如 Redis)
5.3 自定义缓存
实现 org.apache.ibatis.cache.Cache 接口即可集成第三方缓存。
import org.apache.ibatis.cache.Cache;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyCustomCache implements Cache {
private final String id;
private final ConcurrentHashMap<Object, Object> cacheMap = new ConcurrentHashMap<>();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public MyCustomCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
cacheMap.put(key, value);
}
@Override
public Object getObject(Object key) {
return cacheMap.get(key);
}
@Override
public Object removeObject(Object key) {
return cacheMap.remove(key);
}
@Override
public void clear() {
cacheMap.clear();
}
@Override
public int getSize() {
return cacheMap.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}import org.apache.ibatis.cache.Cache;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyCustomCache implements Cache {
private final String id;
private final ConcurrentHashMap<Object, Object> cacheMap = new ConcurrentHashMap<>();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public MyCustomCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
cacheMap.put(key, value);
}
@Override
public Object getObject(Object key) {
return cacheMap.get(key);
}
@Override
public Object removeObject(Object key) {
return cacheMap.remove(key);
}
@Override
public void clear() {
cacheMap.clear();
}
@Override
public int getSize() {
return cacheMap.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}使用 Redis 作为二级缓存(集成 mybatis-redis 或 redis-cache):
import org.mybatis.caches.redis.RedisCache;
// 直接使用 mybatis-redis 提供的实现
public class RedisCacheImpl extends RedisCache {
public RedisCacheImpl(String id) {
super(id);
}
}import org.mybatis.caches.redis.RedisCache;
// 直接使用 mybatis-redis 提供的实现
public class RedisCacheImpl extends RedisCache {
public RedisCacheImpl(String id) {
super(id);
}
}<mapper namespace="com.example.mapper.UserMapper">
<cache type="com.example.cache.RedisCacheImpl"/>
</mapper><mapper namespace="com.example.mapper.UserMapper">
<cache type="com.example.cache.RedisCacheImpl"/>
</mapper>6. MyBatis-Plus 入门
6.1 什么是 MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
核心理念:
- 非侵入式 — 引入 MyBatis-Plus 不会对现有的 MyBatis 架构产生任何影响
- 只做增强 — 不会改变 MyBatis 原有的行为,只是在其基础上增加功能
- 效率提升 — 单表 CRUD 无需手写 SQL 和 XML
官方网站: https://baomidou.com
主要特性:
| 特性 | 说明 |
|---|---|
| 无侵入 | 引入即用,不影响现有项目 |
| CRUD 自动注入 | BaseMapper 提供 20+ 内置方法 |
| 条件构造器 | 链式编程组装查询/更新条件 |
| 分页插件 | 支持多种数据库分页 |
| 代码生成器 | 一键生成 Entity/Mapper/Service/Controller |
| 乐观锁插件 | 自动处理乐观锁 |
| 逻辑删除 | 物理删除转逻辑删除 |
| 自动填充 | 自动处理创建时间、更新时间等 |
| 多数据源 | 支持读写分离、多库切换 |
| 动态表名 | 动态解析表名 |
6.2 MyBatis vs MyBatis-Plus
| 对比维度 | MyBatis | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 手写 SQL | 无需写 SQL,BaseMapper 内置 |
| 分页 | 手写分页 SQL(limit/offset) | 分页插件,自动生成 |
| 条件查询 | 手写动态 SQL | 条件构造器(Lambda 风格) |
| 代码量 | 多(XML + 接口) | 少(注解或零配置) |
| 学习成本 | 低 | 非常低(熟悉 MyBatis 即可) |
| 复杂查询 | 自定义 SQL | 自定义 SQL + 扩展 |
| 更新频率 | 较低 | 活跃(社区生态好) |
6.3 Spring Boot 配置
添加依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency><dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>注意:引入 mybatis-plus-boot-starter 后,不需要再引入 mybatis-spring-boot-starter,因为 MP 已经包含了 MyBatis 的核心依赖。
application.yml 配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
# mapper XML 文件位置
mapper-locations: classpath*:mapper/**/*.xml
# 实体类包名
type-aliases-package: com.example.entity
# 全局配置
global-config:
db-config:
# 主键策略
id-type: auto
# 表前缀
table-prefix: ""
# 逻辑删除字段
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 原生 MyBatis 配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# SQL 日志
logging:
level:
com.example.mapper: debugspring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
# mapper XML 文件位置
mapper-locations: classpath*:mapper/**/*.xml
# 实体类包名
type-aliases-package: com.example.entity
# 全局配置
global-config:
db-config:
# 主键策略
id-type: auto
# 表前缀
table-prefix: ""
# 逻辑删除字段
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 原生 MyBatis 配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# SQL 日志
logging:
level:
com.example.mapper: debug6.4 BaseMapper 接口
创建实体类:
import com.baomidou.mybatisplus.annotation.*;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("user_name")
private String name;
private String email;
private Integer age;
private String phone;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
@Version
private Integer version;
}import com.baomidou.mybatisplus.annotation.*;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("user_name")
private String name;
private String email;
private Integer age;
private String phone;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
@Version
private Integer version;
}创建 Mapper 接口(只需继承 BaseMapper):
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 无需编写任何方法,即可使用内置 CRUD
}import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 无需编写任何方法,即可使用内置 CRUD
}BaseMapper 内置方法大全:
// 插入
int insert(T entity);
// 删除
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(Map<String, Object> columnMap);
int delete(Wrapper<T> wrapper);
int deleteBatchIds(Collection<? extends Serializable> idList);
// 更新
int updateById(T entity);
int update(T entity, Wrapper<T> updateWrapper);
// 查询
T selectById(Serializable id);
List<T> selectBatchIds(Collection<? extends Serializable> idList);
List<T> selectByMap(Map<String, Object> columnMap);
T selectOne(Wrapper<T> queryWrapper);
Long selectCount(Wrapper<T> queryWrapper);
List<T> selectList(Wrapper<T> queryWrapper);
List<T> selectList(IPage<T> page, Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(IPage<T> page, Wrapper<T> queryWrapper);
List<Object> selectObjs(Wrapper<T> queryWrapper);
<P extends IPage<T>> P selectPage(P page, Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, Wrapper<T> queryWrapper);// 插入
int insert(T entity);
// 删除
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(Map<String, Object> columnMap);
int delete(Wrapper<T> wrapper);
int deleteBatchIds(Collection<? extends Serializable> idList);
// 更新
int updateById(T entity);
int update(T entity, Wrapper<T> updateWrapper);
// 查询
T selectById(Serializable id);
List<T> selectBatchIds(Collection<? extends Serializable> idList);
List<T> selectByMap(Map<String, Object> columnMap);
T selectOne(Wrapper<T> queryWrapper);
Long selectCount(Wrapper<T> queryWrapper);
List<T> selectList(Wrapper<T> queryWrapper);
List<T> selectList(IPage<T> page, Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(IPage<T> page, Wrapper<T> queryWrapper);
List<Object> selectObjs(Wrapper<T> queryWrapper);
<P extends IPage<T>> P selectPage(P page, Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, Wrapper<T> queryWrapper);使用示例:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入
public void addUser(User user) {
int rows = userMapper.insert(user);
System.out.println("影响行数:" + rows);
System.out.println("生成的 ID:" + user.getId()); // id 会被回填
}
// 查询所有
public List<User> listAll() {
return userMapper.selectList(null);
}
// 根据 ID 查询
public User getById(Long id) {
return userMapper.selectById(id);
}
// 条件查询
public User getByName(String name) {
return userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getName, name)
);
}
// 更新
public void updateUser(User user) {
userMapper.updateById(user);
}
// 删除
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
}@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入
public void addUser(User user) {
int rows = userMapper.insert(user);
System.out.println("影响行数:" + rows);
System.out.println("生成的 ID:" + user.getId()); // id 会被回填
}
// 查询所有
public List<User> listAll() {
return userMapper.selectList(null);
}
// 根据 ID 查询
public User getById(Long id) {
return userMapper.selectById(id);
}
// 条件查询
public User getByName(String name) {
return userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getName, name)
);
}
// 更新
public void updateUser(User user) {
userMapper.updateById(user);
}
// 删除
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
}6.5 IService 接口(业务层封装)
MyBatis-Plus 提供了 IService 接口和 ServiceImpl 实现类,进一步封装业务层代码。
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
// 自定义业务方法
User findByName(String name);
boolean batchUpdateStatus(List<Long> ids, Integer status);
}import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
// 自定义业务方法
User findByName(String name);
boolean batchUpdateStatus(List<Long> ids, Integer status);
}import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Override
public User findByName(String name) {
return lambdaQuery().eq(User::getName, name).one();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateStatus(List<Long> ids, Integer status) {
return lambdaUpdate()
.set(User::getStatus, status)
.in(User::getId, ids)
.update();
}
}import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Override
public User findByName(String name) {
return lambdaQuery().eq(User::getName, name).one();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchUpdateStatus(List<Long> ids, Integer status) {
return lambdaUpdate()
.set(User::getStatus, status)
.in(User::getId, ids)
.update();
}
}IService 内置方法一览:
// 插入
boolean save(T entity);
boolean saveBatch(Collection<T> entityList);
boolean saveBatch(Collection<T> entityList, int batchSize);
boolean saveOrUpdate(T entity);
boolean saveOrUpdateBatch(Collection<T> entityList);
// 删除
boolean removeById(Serializable id);
boolean removeByMap(Map<String, Object> columnMap);
boolean remove(Wrapper<T> queryWrapper);
boolean removeByIds(Collection<? extends Serializable> idList);
// 更新
boolean updateById(T entity);
boolean update(T entity, Wrapper<T> updateWrapper);
boolean updateBatchById(Collection<T> entityList);
boolean updateBatchById(Collection<T> entityList, int batchSize);
boolean saveOrUpdate(T entity);
boolean saveOrUpdateBatch(Collection<T> entityList);
// 查询
T getById(Serializable id);
T getOne(Wrapper<T> queryWrapper);
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
Map<String, Object> getMap(Wrapper<T> queryWrapper);
long count();
long count(Wrapper<T> queryWrapper);
List<T> list();
List<T> list(Wrapper<T> queryWrapper);
List<T> listByIds(Collection<? extends Serializable> idList);
List<Map<String, Object>> listMaps();
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
Page<T> page(IPage<T> page);
Page<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 链式查询
LambdaQueryChainWrapper<T> lambdaQuery();
// 链式更新
LambdaUpdateChainWrapper<T> lambdaUpdate();// 插入
boolean save(T entity);
boolean saveBatch(Collection<T> entityList);
boolean saveBatch(Collection<T> entityList, int batchSize);
boolean saveOrUpdate(T entity);
boolean saveOrUpdateBatch(Collection<T> entityList);
// 删除
boolean removeById(Serializable id);
boolean removeByMap(Map<String, Object> columnMap);
boolean remove(Wrapper<T> queryWrapper);
boolean removeByIds(Collection<? extends Serializable> idList);
// 更新
boolean updateById(T entity);
boolean update(T entity, Wrapper<T> updateWrapper);
boolean updateBatchById(Collection<T> entityList);
boolean updateBatchById(Collection<T> entityList, int batchSize);
boolean saveOrUpdate(T entity);
boolean saveOrUpdateBatch(Collection<T> entityList);
// 查询
T getById(Serializable id);
T getOne(Wrapper<T> queryWrapper);
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
Map<String, Object> getMap(Wrapper<T> queryWrapper);
long count();
long count(Wrapper<T> queryWrapper);
List<T> list();
List<T> list(Wrapper<T> queryWrapper);
List<T> listByIds(Collection<? extends Serializable> idList);
List<Map<String, Object>> listMaps();
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
Page<T> page(IPage<T> page);
Page<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 链式查询
LambdaQueryChainWrapper<T> lambdaQuery();
// 链式更新
LambdaUpdateChainWrapper<T> lambdaUpdate();7. 核心功能
7.1 条件构造器
条件构造器是 MyBatis-Plus 的灵魂功能,提供链式编程方式构造 SQL 条件。
QueryWrapper
// 基本用法
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.ge("age", 18)
.like("email", "@qq.com")
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);// 基本用法
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.ge("age", 18)
.like("email", "@qq.com")
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);QueryWrapper 完整方法示例:
public void queryWrapperExamples() {
// ==================== 比较操作 ====================
QueryWrapper<User> w = new QueryWrapper<>();
w.eq("name", "张三"); // name = '张三'
w.ne("name", "张三"); // name <> '张三'
w.gt("age", 18); // age > 18
w.ge("age", 18); // age >= 18
w.lt("age", 60); // age < 60
w.le("age", 60); // age <= 60
w.between("age", 18, 60); // age BETWEEN 18 AND 60
w.notBetween("age", 18, 60); // age NOT BETWEEN 18 AND 60
w.like("name", "张"); // name LIKE '%张%'
w.notLike("name", "张"); // name NOT LIKE '%张%'
w.likeLeft("name", "张"); // name LIKE '%张'
w.likeRight("name", "张"); // name LIKE '张%'
w.in("id", 1, 2, 3); // id IN (1,2,3)
w.in("id", Arrays.asList(1,2,3)); // id IN (1,2,3)
w.notIn("id", 1, 2, 3); // id NOT IN (1,2,3)
w.isNull("phone"); // phone IS NULL
w.isNotNull("phone"); // phone IS NOT NULL
// ==================== 逻辑组合 ====================
// AND (默认)
w.eq("name", "张三")
.eq("status", 1); // name = '张三' AND status = 1
// OR
w.eq("name", "张三")
.or()
.eq("name", "李四"); // name = '张三' OR name = '李四'
// 嵌套 OR
w.and(i -> i.eq("name", "张三")
.or()
.eq("name", "李四"))
.eq("status", 1); // (name = '张三' OR name = '李四') AND status = 1
// 嵌套 AND
w.or(i -> i.eq("status", 1)
.eq("deleted", 0)); // OR (status = 1 AND deleted = 0)
// ==================== 排序 ====================
w.orderBy(true, true, "age"); // ORDER BY age ASC
w.orderByAsc("age", "id"); // ORDER BY age ASC, id ASC
w.orderByDesc("create_time"); // ORDER BY create_time DESC
// ==================== 其他 ====================
w.select("id", "name", "email"); // 指定查询字段
w.select(User.class, info -> !info.getColumn().equals("phone")); // 排除字段
w.last("LIMIT 10"); // 追加 SQL 片段(有注入风险)
w.exists("SELECT 1 FROM order WHERE user_id = user.id");
w.notExists("SELECT 1 FROM order WHERE user_id = user.id");
// 执行查询
List<User> users = userMapper.selectList(w);
}public void queryWrapperExamples() {
// ==================== 比较操作 ====================
QueryWrapper<User> w = new QueryWrapper<>();
w.eq("name", "张三"); // name = '张三'
w.ne("name", "张三"); // name <> '张三'
w.gt("age", 18); // age > 18
w.ge("age", 18); // age >= 18
w.lt("age", 60); // age < 60
w.le("age", 60); // age <= 60
w.between("age", 18, 60); // age BETWEEN 18 AND 60
w.notBetween("age", 18, 60); // age NOT BETWEEN 18 AND 60
w.like("name", "张"); // name LIKE '%张%'
w.notLike("name", "张"); // name NOT LIKE '%张%'
w.likeLeft("name", "张"); // name LIKE '%张'
w.likeRight("name", "张"); // name LIKE '张%'
w.in("id", 1, 2, 3); // id IN (1,2,3)
w.in("id", Arrays.asList(1,2,3)); // id IN (1,2,3)
w.notIn("id", 1, 2, 3); // id NOT IN (1,2,3)
w.isNull("phone"); // phone IS NULL
w.isNotNull("phone"); // phone IS NOT NULL
// ==================== 逻辑组合 ====================
// AND (默认)
w.eq("name", "张三")
.eq("status", 1); // name = '张三' AND status = 1
// OR
w.eq("name", "张三")
.or()
.eq("name", "李四"); // name = '张三' OR name = '李四'
// 嵌套 OR
w.and(i -> i.eq("name", "张三")
.or()
.eq("name", "李四"))
.eq("status", 1); // (name = '张三' OR name = '李四') AND status = 1
// 嵌套 AND
w.or(i -> i.eq("status", 1)
.eq("deleted", 0)); // OR (status = 1 AND deleted = 0)
// ==================== 排序 ====================
w.orderBy(true, true, "age"); // ORDER BY age ASC
w.orderByAsc("age", "id"); // ORDER BY age ASC, id ASC
w.orderByDesc("create_time"); // ORDER BY create_time DESC
// ==================== 其他 ====================
w.select("id", "name", "email"); // 指定查询字段
w.select(User.class, info -> !info.getColumn().equals("phone")); // 排除字段
w.last("LIMIT 10"); // 追加 SQL 片段(有注入风险)
w.exists("SELECT 1 FROM order WHERE user_id = user.id");
w.notExists("SELECT 1 FROM order WHERE user_id = user.id");
// 执行查询
List<User> users = userMapper.selectList(w);
}LambdaQueryWrapper
使用 Lambda 表达式,避免硬编码字段名(推荐使用)。
// 方式一:通过实体类获取 lambda
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.ge(User::getAge, 18)
.like(User::getEmail, "@qq.com")
.orderByDesc(User::getCreateTime);
// 方式二:通过 Wrappers 工具类
LambdaQueryWrapper<User> wrapper2 = Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三")
.ge(User::getAge, 18);
// 方式三:链式查询(推荐,最简洁)
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三")
.ge(User::getAge, 18)
.like(User::getEmail, "@qq.com")
);
// 复杂条件
List<User> userList = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.and(w -> w.eq(User::getName, "张三")
.or()
.eq(User::getName, "李四"))
.eq(User::getStatus, 1)
.ne(User::getDeleted, 1)
.orderByDesc(User::getCreateTime)
);// 方式一:通过实体类获取 lambda
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.ge(User::getAge, 18)
.like(User::getEmail, "@qq.com")
.orderByDesc(User::getCreateTime);
// 方式二:通过 Wrappers 工具类
LambdaQueryWrapper<User> wrapper2 = Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三")
.ge(User::getAge, 18);
// 方式三:链式查询(推荐,最简洁)
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三")
.ge(User::getAge, 18)
.like(User::getEmail, "@qq.com")
);
// 复杂条件
List<User> userList = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.and(w -> w.eq(User::getName, "张三")
.or()
.eq(User::getName, "李四"))
.eq(User::getStatus, 1)
.ne(User::getDeleted, 1)
.orderByDesc(User::getCreateTime)
);UpdateWrapper
// 方式一:直接使用实体 + UpdateWrapper
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.set("email", "new@qq.com")
.set("age", 25)
.eq("name", "张三");
userMapper.update(null, wrapper); // 实体传 null,条件从 wrapper 中取
// 方式二:LambdaUpdateWrapper
boolean updated = userService.lambdaUpdate()
.set(User::getEmail, "new@qq.com")
.set(User::getAge, 25)
.eq(User::getName, "张三")
.update();
// 方式三:IService 链式更新
userService.lambdaUpdate()
.set(User::getStatus, 0)
.set(User::getUpdateTime, LocalDateTime.now())
.eq(User::getId, 100L)
.update();
// 方式四:实体更新 + 条件
User user = new User();
user.setEmail("new@qq.com");
user.setAge(25);
UpdateWrapper<User> uw = new UpdateWrapper<>();
uw.eq("name", "张三");
userMapper.update(user, uw);// 方式一:直接使用实体 + UpdateWrapper
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.set("email", "new@qq.com")
.set("age", 25)
.eq("name", "张三");
userMapper.update(null, wrapper); // 实体传 null,条件从 wrapper 中取
// 方式二:LambdaUpdateWrapper
boolean updated = userService.lambdaUpdate()
.set(User::getEmail, "new@qq.com")
.set(User::getAge, 25)
.eq(User::getName, "张三")
.update();
// 方式三:IService 链式更新
userService.lambdaUpdate()
.set(User::getStatus, 0)
.set(User::getUpdateTime, LocalDateTime.now())
.eq(User::getId, 100L)
.update();
// 方式四:实体更新 + 条件
User user = new User();
user.setEmail("new@qq.com");
user.setAge(25);
UpdateWrapper<User> uw = new UpdateWrapper<>();
uw.eq("name", "张三");
userMapper.update(user, uw);7.2 分页插件
配置分页插件:
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL)
);
return interceptor;
}
}import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL)
);
return interceptor;
}
}分页查询使用:
@Service
public class UserPageService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
// 方式一:Mapper 分页
public Page<User> pageByMapper(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
// 查询条件
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime);
// 执行分页查询
return userMapper.selectPage(page, wrapper);
}
// 方式二:IService 分页
public Page<User> pageByService(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1);
return userService.page(page, wrapper);
}
// 方式三:IPage 接口(更通用)
public IPage<User> pageByIPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
return userMapper.selectPage(page, null);
}
// 获取分页结果
public void processPageResult() {
Page<User> page = userMapper.selectPage(
new Page<>(1, 10),
Wrappers.<User>lambdaQuery().eq(User::getStatus, 1)
);
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页大小:" + page.getSize());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("数据列表:" + page.getRecords());
}
// 返回 Map 的分页
public Page<Map<String, Object>> pageMap(int pageNum, int pageSize) {
Page<Map<String, Object>> page = new Page<>(pageNum, pageSize);
return userMapper.selectMapsPage(page, null);
}
// 自定义 SQL 分页
public Page<User> pageCustom(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
// Mapper 中定义自定义分页方法
return userMapper.selectUserPage(page, "张三");
}
}@Service
public class UserPageService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
// 方式一:Mapper 分页
public Page<User> pageByMapper(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
// 查询条件
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime);
// 执行分页查询
return userMapper.selectPage(page, wrapper);
}
// 方式二:IService 分页
public Page<User> pageByService(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1);
return userService.page(page, wrapper);
}
// 方式三:IPage 接口(更通用)
public IPage<User> pageByIPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
return userMapper.selectPage(page, null);
}
// 获取分页结果
public void processPageResult() {
Page<User> page = userMapper.selectPage(
new Page<>(1, 10),
Wrappers.<User>lambdaQuery().eq(User::getStatus, 1)
);
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页大小:" + page.getSize());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("数据列表:" + page.getRecords());
}
// 返回 Map 的分页
public Page<Map<String, Object>> pageMap(int pageNum, int pageSize) {
Page<Map<String, Object>> page = new Page<>(pageNum, pageSize);
return userMapper.selectMapsPage(page, null);
}
// 自定义 SQL 分页
public Page<User> pageCustom(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
// Mapper 中定义自定义分页方法
return userMapper.selectUserPage(page, "张三");
}
}自定义 Mapper 分页方法:
// Mapper 接口
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页查询
* 注意:第一个参数必须是 Page 类型
*/
Page<User> selectUserPage(Page<User> page, @Param("name") String name);
/**
* 自定义分页 + 多表查询
*/
Page<UserVO> selectUserWithOrderPage(Page<UserVO> page,
@Param("status") Integer status);
}// Mapper 接口
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页查询
* 注意:第一个参数必须是 Page 类型
*/
Page<User> selectUserPage(Page<User> page, @Param("name") String name);
/**
* 自定义分页 + 多表查询
*/
Page<UserVO> selectUserWithOrderPage(Page<UserVO> page,
@Param("status") Integer status);
}<!-- UserMapper.xml -->
<!-- 自定义分页查询 -->
<select id="selectUserPage" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 多表分页查询 -->
<select id="selectUserWithOrderPage" resultType="com.example.vo.UserVO">
SELECT u.*, COUNT(o.id) as order_count
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="status != null">
AND u.status = #{status}
</if>
</where>
GROUP BY u.id
ORDER BY u.create_time DESC
</select><!-- UserMapper.xml -->
<!-- 自定义分页查询 -->
<select id="selectUserPage" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 多表分页查询 -->
<select id="selectUserWithOrderPage" resultType="com.example.vo.UserVO">
SELECT u.*, COUNT(o.id) as order_count
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="status != null">
AND u.status = #{status}
</if>
</where>
GROUP BY u.id
ORDER BY u.create_time DESC
</select>7.3 代码生成器
MyBatis-Plus 提供了强大的代码生成器,可以一键生成 Entity、Mapper、Service、Controller 等代码。
FastAutoGenerator(推荐,新版)
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/mydb",
"root", "password")
// 全局配置
.globalConfig(builder -> {
builder.author("开发者姓名") // 作者
.outputDir("D://code-gen") // 输出目录
.commentDate("yyyy-MM-dd") // 注释日期
.disableOpenDir(); // 生成后不打开目录
})
// 包配置
.packageConfig(builder -> {
builder.parent("com.example") // 父包名
.moduleName("system") // 模块名(可选)
.entity("entity") // 实体类包名
.service("service") // Service 包名
.serviceImpl("service.impl") // ServiceImpl 包名
.mapper("mapper") // Mapper 包名
.xml("mapper.xml") // XML 包名
.controller("controller"); // Controller 包名
})
// 策略配置
.strategyConfig(builder -> {
builder.addInclude("user", "order", "product") // 表名
.addTablePrefix("t_", "sys_"); // 表前缀过滤
// 实体类策略
builder.entityBuilder()
.enableLombok() // 使用 Lombok
.enableTableFieldAnnotation() // 添加 @TableField
.logicDeleteColumnName("deleted") // 逻辑删除字段
.versionColumnName("version"); // 乐观锁字段
// Service 策略
builder.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl");
// Mapper 策略
builder.mapperBuilder()
.enableBaseResultMap() // 生成 resultMap
.enableBaseColumnList(); // 生成 baseColumns
// Controller 策略
builder.controllerBuilder()
.enableHyphenStyle() // 连字符 URL
.enableRestStyle(); // @RestController
})
// 模板引擎(默认 Velocity,可选 Freemarker/Beetl)
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/mydb",
"root", "password")
// 全局配置
.globalConfig(builder -> {
builder.author("开发者姓名") // 作者
.outputDir("D://code-gen") // 输出目录
.commentDate("yyyy-MM-dd") // 注释日期
.disableOpenDir(); // 生成后不打开目录
})
// 包配置
.packageConfig(builder -> {
builder.parent("com.example") // 父包名
.moduleName("system") // 模块名(可选)
.entity("entity") // 实体类包名
.service("service") // Service 包名
.serviceImpl("service.impl") // ServiceImpl 包名
.mapper("mapper") // Mapper 包名
.xml("mapper.xml") // XML 包名
.controller("controller"); // Controller 包名
})
// 策略配置
.strategyConfig(builder -> {
builder.addInclude("user", "order", "product") // 表名
.addTablePrefix("t_", "sys_"); // 表前缀过滤
// 实体类策略
builder.entityBuilder()
.enableLombok() // 使用 Lombok
.enableTableFieldAnnotation() // 添加 @TableField
.logicDeleteColumnName("deleted") // 逻辑删除字段
.versionColumnName("version"); // 乐观锁字段
// Service 策略
builder.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl");
// Mapper 策略
builder.mapperBuilder()
.enableBaseResultMap() // 生成 resultMap
.enableBaseColumnList(); // 生成 baseColumns
// Controller 策略
builder.controllerBuilder()
.enableHyphenStyle() // 连字符 URL
.enableRestStyle(); // @RestController
})
// 模板引擎(默认 Velocity,可选 Freemarker/Beetl)
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}AutoGenerator(经典版)
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class ClassicCodeGenerator {
public static void main(String[] args) {
AutoGenerator generator = new AutoGenerator();
// 数据源配置
generator.setDataSource(new DataSourceConfig.Builder(
"jdbc:mysql://localhost:3306/mydb", "root", "password")
.build()
);
// 全局配置
generator.setGlobalConfig(new GlobalConfig.Builder()
.outputDir("D://code-gen")
.author("开发者姓名")
.commentDate("yyyy-MM-dd")
.disableOpenDir()
.build()
);
// 包配置
generator.setPackageInfo(new PackageConfig.Builder()
.parent("com.example")
.moduleName("system")
.entity("entity")
.service("service")
.serviceImpl("service.impl")
.mapper("mapper")
.xml("mapper.xml")
.controller("controller")
.build()
);
// 策略配置
generator.setStrategy(new StrategyConfig.Builder()
.addInclude("user", "order")
.addTablePrefix("t_")
.entityBuilder()
.enableLombok()
.naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel)
.logicDeleteColumnName("deleted")
.versionColumnName("version")
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
.mapperBuilder()
.enableBaseResultMap()
.enableBaseColumnList()
.build()
);
generator.execute();
}
}import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class ClassicCodeGenerator {
public static void main(String[] args) {
AutoGenerator generator = new AutoGenerator();
// 数据源配置
generator.setDataSource(new DataSourceConfig.Builder(
"jdbc:mysql://localhost:3306/mydb", "root", "password")
.build()
);
// 全局配置
generator.setGlobalConfig(new GlobalConfig.Builder()
.outputDir("D://code-gen")
.author("开发者姓名")
.commentDate("yyyy-MM-dd")
.disableOpenDir()
.build()
);
// 包配置
generator.setPackageInfo(new PackageConfig.Builder()
.parent("com.example")
.moduleName("system")
.entity("entity")
.service("service")
.serviceImpl("service.impl")
.mapper("mapper")
.xml("mapper.xml")
.controller("controller")
.build()
);
// 策略配置
generator.setStrategy(new StrategyConfig.Builder()
.addInclude("user", "order")
.addTablePrefix("t_")
.entityBuilder()
.enableLombok()
.naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel)
.logicDeleteColumnName("deleted")
.versionColumnName("version")
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
.mapperBuilder()
.enableBaseResultMap()
.enableBaseColumnList()
.build()
);
generator.execute();
}
}7.4 乐观锁插件
乐观锁用于解决并发更新时的数据冲突问题。
实现原理:
- 读取数据时获取版本号
version - 更新数据时
SET version = version + 1 - 更新条件中带上原
version - 如果
version已被其他线程修改,则更新失败(影响行数为 0)
配置乐观锁插件:
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(顺序重要:先分页后乐观锁)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(顺序重要:先分页后乐观锁)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}实体类配置:
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
@Version // 乐观锁注解
private Integer version;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
@Version // 乐观锁注解
private Integer version;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}使用示例:
@Service
public class OptimisticLockService {
@Autowired
private UserMapper userMapper;
/**
* 常规更新(乐观锁生效)
*/
public void updateWithLock() {
// 1. 先查询,获取 version
User user = userMapper.selectById(1L);
// user.getVersion() = 1
// 2. 修改数据
user.setName("新名字");
user.setAge(30);
// 3. 更新(自动带上 version 条件)
int rows = userMapper.updateById(user);
// 执行的 SQL: UPDATE user SET name='新名字', age=30, version=2
// WHERE id=1 AND version=1
//
// 如果返回 rows == 0,说明 version 已被修改,需要重试
if (rows == 0) {
throw new RuntimeException("数据已被他人修改,请刷新后重试");
}
}
/**
* 重试机制(简单版)
*/
public void updateWithRetry() {
int retryCount = 3;
while (retryCount-- > 0) {
User user = userMapper.selectById(1L);
user.setName("新名字");
int rows = userMapper.updateById(user);
if (rows > 0) {
return; // 更新成功
}
// 更新失败,等待后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", e);
}
}
throw new RuntimeException("更新失败,重试次数已用尽");
}
/**
* 批量更新注意:乐观锁不支持批量更新
*/
public void batchUpdate() {
// 错误方式:updateBatchById 不会逐条添加 version 条件
// userService.updateBatchById(userList);
// 正确方式:逐条更新
List<User> userList = userMapper.selectList(null);
userList.forEach(user -> {
user.setName("批量改名");
userMapper.updateById(user);
});
}
}@Service
public class OptimisticLockService {
@Autowired
private UserMapper userMapper;
/**
* 常规更新(乐观锁生效)
*/
public void updateWithLock() {
// 1. 先查询,获取 version
User user = userMapper.selectById(1L);
// user.getVersion() = 1
// 2. 修改数据
user.setName("新名字");
user.setAge(30);
// 3. 更新(自动带上 version 条件)
int rows = userMapper.updateById(user);
// 执行的 SQL: UPDATE user SET name='新名字', age=30, version=2
// WHERE id=1 AND version=1
//
// 如果返回 rows == 0,说明 version 已被修改,需要重试
if (rows == 0) {
throw new RuntimeException("数据已被他人修改,请刷新后重试");
}
}
/**
* 重试机制(简单版)
*/
public void updateWithRetry() {
int retryCount = 3;
while (retryCount-- > 0) {
User user = userMapper.selectById(1L);
user.setName("新名字");
int rows = userMapper.updateById(user);
if (rows > 0) {
return; // 更新成功
}
// 更新失败,等待后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", e);
}
}
throw new RuntimeException("更新失败,重试次数已用尽");
}
/**
* 批量更新注意:乐观锁不支持批量更新
*/
public void batchUpdate() {
// 错误方式:updateBatchById 不会逐条添加 version 条件
// userService.updateBatchById(userList);
// 正确方式:逐条更新
List<User> userList = userMapper.selectList(null);
userList.forEach(user -> {
user.setName("批量改名");
userMapper.updateById(user);
});
}
}乐观锁注意事项:
- 仅支持
updateById(id)和update(entity, wrapper)方法 - 不支持批量更新
updateBatchById version字段类型支持int、Integer、long、Long、Date、Timestamp- 更新时
version会自增,条件中会带上原来的version
7.5 逻辑删除
逻辑删除是在数据表中增加一个删除标记字段,删除操作实际为更新该字段。
配置逻辑删除:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 已删除的值
logic-not-delete-value: 0 # 未删除的值mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 已删除的值
logic-not-delete-value: 0 # 未删除的值实体类配置:
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableLogic // 逻辑删除注解
private Integer deleted;
}@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableLogic // 逻辑删除注解
private Integer deleted;
}使用示例:
@Service
public class LogicDeleteService {
@Autowired
private UserMapper userMapper;
/**
* 逻辑删除(实际执行 UPDATE)
*/
public void logicDelete(Long id) {
// 执行的 SQL: UPDATE user SET deleted=1 WHERE id=1 AND deleted=0
userMapper.deleteById(id);
}
/**
* 查询自动带上逻辑删除条件
*/
public User selectById(Long id) {
// 执行的 SQL: SELECT * FROM user WHERE id=1 AND deleted=0
return userMapper.selectById(id);
}
/**
* 条件查询自动追加逻辑删除条件
*/
public List<User> selectByCondition() {
// 执行的 SQL: SELECT * FROM user WHERE name LIKE '%张%' AND deleted=0
return userMapper.selectList(
Wrappers.<User>lambdaQuery().like(User::getName, "张")
);
}
/**
* 查询包含已逻辑删除的数据(不自动追加条件)
*/
public List<User> selectIncludeDeleted() {
return userMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(User::getName, "张")
// 手动清除逻辑删除条件
.last("AND 1=1") // 或者不用 last,自己拼完整条件
);
}
}@Service
public class LogicDeleteService {
@Autowired
private UserMapper userMapper;
/**
* 逻辑删除(实际执行 UPDATE)
*/
public void logicDelete(Long id) {
// 执行的 SQL: UPDATE user SET deleted=1 WHERE id=1 AND deleted=0
userMapper.deleteById(id);
}
/**
* 查询自动带上逻辑删除条件
*/
public User selectById(Long id) {
// 执行的 SQL: SELECT * FROM user WHERE id=1 AND deleted=0
return userMapper.selectById(id);
}
/**
* 条件查询自动追加逻辑删除条件
*/
public List<User> selectByCondition() {
// 执行的 SQL: SELECT * FROM user WHERE name LIKE '%张%' AND deleted=0
return userMapper.selectList(
Wrappers.<User>lambdaQuery().like(User::getName, "张")
);
}
/**
* 查询包含已逻辑删除的数据(不自动追加条件)
*/
public List<User> selectIncludeDeleted() {
return userMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(User::getName, "张")
// 手动清除逻辑删除条件
.last("AND 1=1") // 或者不用 last,自己拼完整条件
);
}
}逻辑删除绕过方式:
// 方式一:使用原生 MyBatis 查询(不走 MP 拦截)
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectByIdRaw(Long id);
}
// 方式二:在 Wrapper 中忽略逻辑删除(3.5.x 版本)
List<User> users = userMapper.selectList(
new QueryWrapper<User>().lambda()
.eq(User::getId, 1L)
// 使用 condition 参数控制
);// 方式一:使用原生 MyBatis 查询(不走 MP 拦截)
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectByIdRaw(Long id);
}
// 方式二:在 Wrapper 中忽略逻辑删除(3.5.x 版本)
List<User> users = userMapper.selectList(
new QueryWrapper<User>().lambda()
.eq(User::getId, 1L)
// 使用 condition 参数控制
);逻辑删除的优缺点:
| 优点 | 缺点 |
|---|---|
| 数据不丢失,可恢复 | 查询需要额外加条件 |
| 保留历史数据 | 唯一索引冲突问题 |
| 实现软删除简单 | 数据量大时影响查询性能 |
7.6 自动填充
自动填充用于自动处理创建时间、更新时间、创建人等公共字段。
实现 MetaObjectHandler:
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 自动填充创建时间和更新时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 自动填充创建人(从请求上下文获取)
this.strictInsertFill(metaObject, "createBy", () -> getCurrentUserId(), String.class);
// 自动填充逻辑删除默认值
this.strictInsertFill(metaObject, "deleted", () -> 0, Integer.class);
}
@Override
public void updateFill(MetaObject metaObject) {
// 自动填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 自动填充更新人
this.strictUpdateFill(metaObject, "updateBy", () -> getCurrentUserId(), String.class);
}
/**
* 获取当前登录用户(示例,需根据实际情况实现)
*/
private String getCurrentUserId() {
// 从 SecurityContext / 请求头 中获取
return "system";
}
}import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 自动填充创建时间和更新时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 自动填充创建人(从请求上下文获取)
this.strictInsertFill(metaObject, "createBy", () -> getCurrentUserId(), String.class);
// 自动填充逻辑删除默认值
this.strictInsertFill(metaObject, "deleted", () -> 0, Integer.class);
}
@Override
public void updateFill(MetaObject metaObject) {
// 自动填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// 自动填充更新人
this.strictUpdateFill(metaObject, "updateBy", () -> getCurrentUserId(), String.class);
}
/**
* 获取当前登录用户(示例,需根据实际情况实现)
*/
private String getCurrentUserId() {
// 从 SecurityContext / 请求头 中获取
return "system";
}
}实体类配置:
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private String createBy;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充
private String updateBy;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private Integer deleted;
}import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private String createBy;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充
private String updateBy;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private Integer deleted;
}FieldFill 可选值:
| 值 | 说明 |
|---|---|
DEFAULT | 默认不处理 |
INSERT | 插入时填充 |
UPDATE | 更新时填充 |
INSERT_UPDATE | 插入和更新时都填充 |
注意事项:
- 如果实体类显式设置了值,自动填充不会覆盖(除非使用
setFieldValByName强制填充) strictInsertFill/strictUpdateFill会检查字段是否有值,有值则不填充- 使用
fillStrategy方法可以自定义填充策略
7.7 多数据源
MyBatis-Plus 提供了 dynamic-datasource 组件,支持多数据源和读写分离。
添加依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency><dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>application.yml 配置:
spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 是否严格模式
datasource:
master: # 主库
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1: # 从库 1
url: jdbc:mysql://192.168.1.100:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2: # 从库 2
url: jdbc:mysql://192.168.1.101:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
biz_db: # 业务库
url: jdbc:mysql://localhost:3306/biz_db?useUnicode=true&characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driverspring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 是否严格模式
datasource:
master: # 主库
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1: # 从库 1
url: jdbc:mysql://192.168.1.100:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2: # 从库 2
url: jdbc:mysql://192.168.1.101:3306/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
biz_db: # 业务库
url: jdbc:mysql://localhost:3306/biz_db?useUnicode=true&characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver使用 @DS 注解切换数据源:
import com.baomidou.dynamic.datasource.annotation.DS;
@Service
@DS("master") // 类级别,默认走主库
public class UserService {
@Autowired
private UserMapper userMapper;
// 读操作走从库
@DS("slave_1")
public User getById(Long id) {
return userMapper.selectById(id);
}
// 写操作走主库(默认)
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
userMapper.insert(user);
}
// 跨数据源操作
@DS("master")
public void crossDataSource() {
// 当前方法在主库
User user = userMapper.selectById(1L);
// 内部调用走 slave_1
User other = getById(1L);
}
}
// 另一个 Service 使用不同的数据源
@Service
@DS("biz_db")
public class BizService {
@Autowired
private BizMapper bizMapper;
public List<Biz> listBiz() {
return bizMapper.selectList(null);
}
}import com.baomidou.dynamic.datasource.annotation.DS;
@Service
@DS("master") // 类级别,默认走主库
public class UserService {
@Autowired
private UserMapper userMapper;
// 读操作走从库
@DS("slave_1")
public User getById(Long id) {
return userMapper.selectById(id);
}
// 写操作走主库(默认)
@Transactional(rollbackFor = Exception.class)
public void addUser(User user) {
userMapper.insert(user);
}
// 跨数据源操作
@DS("master")
public void crossDataSource() {
// 当前方法在主库
User user = userMapper.selectById(1L);
// 内部调用走 slave_1
User other = getById(1L);
}
}
// 另一个 Service 使用不同的数据源
@Service
@DS("biz_db")
public class BizService {
@Autowired
private BizMapper bizMapper;
public List<Biz> listBiz() {
return bizMapper.selectList(null);
}
}注解与 Mapper 绑定:
@DS("slave_1")
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 当前 Mapper 所有操作默认走 slave_1
}
@DS("biz_db")
@Mapper
public interface BizMapper extends BaseMapper<Biz> {
// 当前 Mapper 走 biz_db
}@DS("slave_1")
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 当前 Mapper 所有操作默认走 slave_1
}
@DS("biz_db")
@Mapper
public interface BizMapper extends BaseMapper<Biz> {
// 当前 Mapper 走 biz_db
}7.8 其他插件
防全表更新/删除插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 防全表更新与删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 防全表更新与删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}// 当执行无条件的 update/delete 时会抛出异常
// DELETE FROM user; -- 抛出异常
// UPDATE user SET name='xx'; -- 抛出异常
// 正确方式:必须带条件
userMapper.delete(Wrappers.<User>lambdaQuery().eq(User::getId, 1L));// 当执行无条件的 update/delete 时会抛出异常
// DELETE FROM user; -- 抛出异常
// UPDATE user SET name='xx'; -- 抛出异常
// 正确方式:必须带条件
userMapper.delete(Wrappers.<User>lambdaQuery().eq(User::getId, 1L));SQL 性能分析插件
@Bean
@Profile({"dev", "test"}) // 只在开发/测试环境开启
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor interceptor = new PerformanceInterceptor();
interceptor.setMaxTime(1000); // SQL 执行超时时间(ms)
interceptor.setFormat(true); // 格式化 SQL
return interceptor;
}@Bean
@Profile({"dev", "test"}) // 只在开发/测试环境开启
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor interceptor = new PerformanceInterceptor();
interceptor.setMaxTime(1000); // SQL 执行超时时间(ms)
interceptor.setFormat(true); // 格式化 SQL
return interceptor;
}动态表名插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 动态表名(按年份分表)
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor() {{
setTableNameHandler((sql, tableName) -> {
String year = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy"));
return tableName + "_" + year; // user -> user_2026
});
}});
return interceptor;
}@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 动态表名(按年份分表)
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor() {{
setTableNameHandler((sql, tableName) -> {
String year = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy"));
return tableName + "_" + year; // user -> user_2026
});
}});
return interceptor;
}8. 事务管理
8.1 @Transactional 使用
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserTransactionalService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 基本事务:所有操作在同一个事务中
*/
@Transactional(rollbackFor = Exception.class)
public void createUserWithOrder(User user, Order order) {
userMapper.insert(user);
// 如果此处抛出异常,上面的 insert 也会回滚
order.setUserId(user.getId());
orderMapper.insert(order);
}
/**
* 事务传播行为
*/
@Transactional(
propagation = Propagation.REQUIRED, // 事务传播行为
isolation = Isolation.READ_COMMITTED, // 事务隔离级别
timeout = 30, // 超时时间(秒)
rollbackFor = Exception.class, // 指定哪些异常回滚
noRollbackFor = BusinessException.class, // 指定哪些异常不回滚
readOnly = false // 是否只读
)
public void createWithConfig() {
// 业务逻辑
}
/**
* 只读事务(优化查询性能)
*/
@Transactional(readOnly = true)
public User getById(Long id) {
return userMapper.selectById(id);
}
}import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserTransactionalService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 基本事务:所有操作在同一个事务中
*/
@Transactional(rollbackFor = Exception.class)
public void createUserWithOrder(User user, Order order) {
userMapper.insert(user);
// 如果此处抛出异常,上面的 insert 也会回滚
order.setUserId(user.getId());
orderMapper.insert(order);
}
/**
* 事务传播行为
*/
@Transactional(
propagation = Propagation.REQUIRED, // 事务传播行为
isolation = Isolation.READ_COMMITTED, // 事务隔离级别
timeout = 30, // 超时时间(秒)
rollbackFor = Exception.class, // 指定哪些异常回滚
noRollbackFor = BusinessException.class, // 指定哪些异常不回滚
readOnly = false // 是否只读
)
public void createWithConfig() {
// 业务逻辑
}
/**
* 只读事务(优化查询性能)
*/
@Transactional(readOnly = true)
public User getById(Long id) {
return userMapper.selectById(id);
}
}事务传播行为:
| 传播行为 | 说明 |
|---|---|
REQUIRED (默认) | 支持当前事务,不存在则创建新事务 |
REQUIRES_NEW | 创建新事务,挂起当前事务 |
NESTED | 嵌套事务(JDBC 3.0 Savepoint) |
SUPPORTS | 支持当前事务,不存在则以非事务运行 |
NOT_SUPPORTED | 以非事务方式运行,挂起当前事务 |
NEVER | 以非事务方式运行,存在事务则抛异常 |
MANDATORY | 必须在事务中运行,无事务则抛异常 |
8.2 @Transactional 事务失效的 N 种场景(面试必考)
场景 1:自调用(最常见)
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 事务生效
}
// 同一个类中调用,事务不生效!
public void batchCreate() {
this.createOrder(); // this 调用,绕过 AOP 代理
}
}@Service
public class OrderService {
@Transactional
public void createOrder() {
// 事务生效
}
// 同一个类中调用,事务不生效!
public void batchCreate() {
this.createOrder(); // this 调用,绕过 AOP 代理
}
}原因: @Transactional 基于 AOP 代理,this.xxx() 调用的是目标对象方法,不会经过代理。解决: 注入自身(@Autowired private OrderService self),或拆分到另一个 Service。
场景 2:非 public 方法
@Transactional
private void doSave() { // 事务不生效
// ...
}@Transactional
private void doSave() { // 事务不生效
// ...
}场景 3:异常被 catch 吞掉
@Transactional
public void save() {
try {
// 数据库操作
throw new RuntimeException("出错");
} catch (Exception e) {
log.error("出错了", e); // 吞掉了异常,不回滚!
}
}@Transactional
public void save() {
try {
// 数据库操作
throw new RuntimeException("出错");
} catch (Exception e) {
log.error("出错了", e); // 吞掉了异常,不回滚!
}
}原因: 事务只回滚未被捕获的 RuntimeException 和 Error。解决: 在 catch 中手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 或重新抛出异常。
场景 4:rollbackFor 没配对
@Transactional // 默认只回滚 RuntimeException 和 Error
public void save() throws Exception {
throw new Exception("非 RuntimeException,不回滚!");
}
// 正确写法
@Transactional(rollbackFor = Exception.class) // 任何异常都回滚
public void save() throws Exception {
throw new Exception("回滚了");
}@Transactional // 默认只回滚 RuntimeException 和 Error
public void save() throws Exception {
throw new Exception("非 RuntimeException,不回滚!");
}
// 正确写法
@Transactional(rollbackFor = Exception.class) // 任何异常都回滚
public void save() throws Exception {
throw new Exception("回滚了");
}场景 5:final 方法
@Service
public class OrderService {
@Transactional
public final void createOrder() { // CGLIB 无法代理 final 方法
// 事务不生效
}
}@Service
public class OrderService {
@Transactional
public final void createOrder() { // CGLIB 无法代理 final 方法
// 事务不生效
}
}场景 6:多线程
@Transactional
public void save() {
new Thread(() -> {
userMapper.insert(user); // 新线程,不在当前事务中
}).start();
}@Transactional
public void save() {
new Thread(() -> {
userMapper.insert(user); // 新线程,不在当前事务中
}).start();
}场景 7:数据库引擎不支持事务
MyISAM 引擎不支持事务,只有 InnoDB 支持。MySQL 5.5 后默认 InnoDB。
总结排查思路:
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 自调用(this.xxx) | 绕过 AOP 代理 | 注入自身 / 拆到另一个 Service |
| 非 public 方法 | AOP 不代理 private/protected | 改为 public |
| 异常被 catch | 事务感知不到异常 | 手动 setRollbackOnly / 重新抛出 |
| rollbackFor 未配 | 只回滚 RuntimeException | 加 rollbackFor = Exception.class |
| final 方法 | CGLIB 不能代理 final | 去掉 final |
| 多线程 | 新线程不在事务中 | 事务不跨线程传播 |
| 非 InnoDB | 引擎不支持事务 | 换 InnoDB |
8.3 多数据源下的事务
使用 @DSTransactional 或 Seata 处理分布式事务。
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
@Service
public class MultiDataSourceService {
@Autowired
private UserMapper userMapper;
@Autowired
private BizMapper bizMapper;
/**
* 多数据源本地事务(使用 DSTransactional)
* 基于本地事务 + 最后提交保证
*/
@DSTransactional
public void crossDataSourceSave(User user, Biz biz) {
userMapper.insert(user); // 走默认数据源(master)
bizMapper.insert(biz); // 走 biz_db
// 两个操作要么都成功,要么都回滚
}
/**
* 分布式事务(基于 Seata)
*/
@GlobalTransactional
@DSTransactional
public void distributedTransaction() {
// 跨服务、跨数据库的分布式事务
}
}import com.baomidou.dynamic.datasource.annotation.DSTransactional;
@Service
public class MultiDataSourceService {
@Autowired
private UserMapper userMapper;
@Autowired
private BizMapper bizMapper;
/**
* 多数据源本地事务(使用 DSTransactional)
* 基于本地事务 + 最后提交保证
*/
@DSTransactional
public void crossDataSourceSave(User user, Biz biz) {
userMapper.insert(user); // 走默认数据源(master)
bizMapper.insert(biz); // 走 biz_db
// 两个操作要么都成功,要么都回滚
}
/**
* 分布式事务(基于 Seata)
*/
@GlobalTransactional
@DSTransactional
public void distributedTransaction() {
// 跨服务、跨数据库的分布式事务
}
}@DSTransactional 的局限性:
- 使用
@DSTransactional时不能同时使用 Spring 的@Transactional - 基于
ConnectionFactory实现,非 XA 事务 - 适用于大多数多数据源场景,但不适用于严格的金融级场景
- 严格分布式事务建议使用 Seata、ShardingSphere 或 TCC 模式
9. 最佳实践
9.1 Maven 依赖版本选择
MyBatis-Plus 版本兼容性:
| MyBatis-Plus 版本 | Spring Boot 版本 | MyBatis 版本 | Java 版本 |
|---|---|---|---|
| 3.5.x | 2.7.x / 3.0.x | 3.5.x | Java 8+ |
| 3.4.x | 2.3.x - 2.6.x | 3.5.x | Java 8+ |
| 3.3.x | 2.1.x - 2.2.x | 3.4.x | Java 8+ |
推荐的 Maven 依赖(Spring Boot 3.x + MyBatis-Plus 3.5.x):
<!-- Spring Boot 3.x -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<dependencies>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.6</version>
</dependency>
<!-- 模板引擎(代码生成器需要) -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<!-- Lombok(推荐) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies><!-- Spring Boot 3.x -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<dependencies>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.6</version>
</dependency>
<!-- 模板引擎(代码生成器需要) -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<!-- Lombok(推荐) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>9.2 避免 N+1 查询
N+1 查询是最常见的性能问题,指执行 1 次主查询后,又执行了 N 次子查询。
问题示例:
// 错误写法:循环中执行查询
@Service
public class WrongService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
// N+1 问题!
public List<User> getUsersWithOrders() {
List<User> users = userMapper.selectList(null); // 1 次查询
for (User user : users) {
// 每个用户执行一次订单查询(N 次查询)
List<Order> orders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery().eq(Order::getUserId, user.getId())
);
user.setOrders(orders);
}
return users;
}
}// 错误写法:循环中执行查询
@Service
public class WrongService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
// N+1 问题!
public List<User> getUsersWithOrders() {
List<User> users = userMapper.selectList(null); // 1 次查询
for (User user : users) {
// 每个用户执行一次订单查询(N 次查询)
List<Order> orders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery().eq(Order::getUserId, user.getId())
);
user.setOrders(orders);
}
return users;
}
}解决方案:
// 方案一:使用 IN 查询批量加载
@Service
public class BetterService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
public List<User> getUsersWithOrders() {
// 1 次查询获取用户
List<User> users = userMapper.selectList(null);
// 一次性查出所有订单
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Order> allOrders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery().in(Order::getUserId, userIds)
);
// 内存中分组
Map<Long, List<Order>> orderMap = allOrders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
// 组装
users.forEach(user -> user.setOrders(
orderMap.getOrDefault(user.getId(), Collections.emptyList())
));
return users;
}
}
// 方案二:使用 MyBatis 的嵌套查询(一次 JOIN)
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<UserVO> selectUserWithOrders();
}// 方案一:使用 IN 查询批量加载
@Service
public class BetterService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
public List<User> getUsersWithOrders() {
// 1 次查询获取用户
List<User> users = userMapper.selectList(null);
// 一次性查出所有订单
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Order> allOrders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery().in(Order::getUserId, userIds)
);
// 内存中分组
Map<Long, List<Order>> orderMap = allOrders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
// 组装
users.forEach(user -> user.setOrders(
orderMap.getOrDefault(user.getId(), Collections.emptyList())
));
return users;
}
}
// 方案二:使用 MyBatis 的嵌套查询(一次 JOIN)
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<UserVO> selectUserWithOrders();
}<resultMap id="userWithOrdersMap" type="com.example.vo.UserVO">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.*, o.id AS order_id, o.order_no, o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
ORDER BY u.id
</select><resultMap id="userWithOrdersMap" type="com.example.vo.UserVO">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.*, o.id AS order_id, o.order_no, o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
ORDER BY u.id
</select>// 方案三:使用 MyBatis-Plus 的批量方法
List<User> users = userService.list();
List<Order> orders = orderService.list(
Wrappers.<Order>lambdaQuery()
.in(Order::getUserId, users.stream().map(User::getId).collect(Collectors.toList()))
);// 方案三:使用 MyBatis-Plus 的批量方法
List<User> users = userService.list();
List<Order> orders = orderService.list(
Wrappers.<Order>lambdaQuery()
.in(Order::getUserId, users.stream().map(User::getId).collect(Collectors.toList()))
);9.3 BaseMapper 与 IService 的选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 简单单表 CRUD | IService | 内置 saveBatch、page 等方法,代码更少 |
| 复杂查询 | BaseMapper | 直接使用 Wrapper,更灵活 |
| 需要批量操作 | IService | 内置 saveBatch、updateBatchById |
| 自定义 SQL | BaseMapper + XML | 需要手写 SQL 时使用 |
| 事务性业务方法 | IService | @Transactional + 链式调用更简洁 |
| 分页查询 | BaseMapper / IService | 二者都支持 |
混合使用示例:
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
/**
* 使用 IService 的批量方法
*/
@Transactional(rollbackFor = Exception.class)
public void batchSave(List<User> users) {
saveBatch(users, 1000); // 每 1000 条一批
}
/**
* 使用 BaseMapper 的复杂查询
*/
public List<User> complexQuery(String name, Integer minAge, Integer maxAge) {
return baseMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(minAge != null, User::getAge, minAge)
.le(maxAge != null, User::getAge, maxAge)
.orderByDesc(User::getCreateTime)
);
}
/**
* 使用 baseMapper 调用自定义方法
*/
public Page<UserVO> pageWithOrders(Page<UserVO> page) {
return baseMapper.selectUserWithOrdersPage(page);
}
}@Service
public class UserService extends ServiceImpl<UserMapper, User> {
/**
* 使用 IService 的批量方法
*/
@Transactional(rollbackFor = Exception.class)
public void batchSave(List<User> users) {
saveBatch(users, 1000); // 每 1000 条一批
}
/**
* 使用 BaseMapper 的复杂查询
*/
public List<User> complexQuery(String name, Integer minAge, Integer maxAge) {
return baseMapper.selectList(
Wrappers.<User>lambdaQuery()
.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(minAge != null, User::getAge, minAge)
.le(maxAge != null, User::getAge, maxAge)
.orderByDesc(User::getCreateTime)
);
}
/**
* 使用 baseMapper 调用自定义方法
*/
public Page<UserVO> pageWithOrders(Page<UserVO> page) {
return baseMapper.selectUserWithOrdersPage(page);
}
}9.4 多表查询方案
方案一:注解 @Results + 自定义 SQL
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Results(id = "userWithOrderMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "name", column = "name"),
@Result(property = "orders", column = "id",
many = @Many(select = "com.example.mapper.OrderMapper.findByUserId"))
})
@Select("SELECT * FROM user WHERE id = #{id}")
User selectUserWithOrders(Long id);
}@Mapper
public interface UserMapper extends BaseMapper<User> {
@Results(id = "userWithOrderMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "name", column = "name"),
@Result(property = "orders", column = "id",
many = @Many(select = "com.example.mapper.OrderMapper.findByUserId"))
})
@Select("SELECT * FROM user WHERE id = #{id}")
User selectUserWithOrders(Long id);
}方案二:自定义 XML(推荐)
<mapper namespace="com.example.mapper.UserMapper">
<!-- 多表关联查询 -->
<select id="selectUserVO" resultType="com.example.vo.UserVO">
SELECT
u.id,
u.name,
u.email,
u.age,
COUNT(o.id) AS orderCount,
SUM(o.amount) AS totalAmount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="name != null and name != ''">
AND u.name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
GROUP BY u.id
ORDER BY u.id DESC
</select>
<!-- 多表分页查询 -->
<select id="selectUserWithOrderPage" resultType="com.example.vo.UserVO">
SELECT
u.*,
o.id AS orderId,
o.order_no AS orderNo,
o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="ew != null and ew.customSqlSegment != null">
${ew.customSqlSegment}
</if>
</where>
</select>
</mapper><mapper namespace="com.example.mapper.UserMapper">
<!-- 多表关联查询 -->
<select id="selectUserVO" resultType="com.example.vo.UserVO">
SELECT
u.id,
u.name,
u.email,
u.age,
COUNT(o.id) AS orderCount,
SUM(o.amount) AS totalAmount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="name != null and name != ''">
AND u.name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
GROUP BY u.id
ORDER BY u.id DESC
</select>
<!-- 多表分页查询 -->
<select id="selectUserWithOrderPage" resultType="com.example.vo.UserVO">
SELECT
u.*,
o.id AS orderId,
o.order_no AS orderNo,
o.amount
FROM user u
LEFT JOIN order o ON u.id = o.user_id
<where>
<if test="ew != null and ew.customSqlSegment != null">
${ew.customSqlSegment}
</if>
</where>
</select>
</mapper>// 自定义 XML + MP 条件构造器
public interface UserMapper extends BaseMapper<User> {
Page<UserVO> selectUserWithOrderPage(Page<UserVO> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
}// 自定义 XML + MP 条件构造器
public interface UserMapper extends BaseMapper<User> {
Page<UserVO> selectUserWithOrderPage(Page<UserVO> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
}方案三:视图对象(VO)方式
@Data
public class UserVO {
private Long id;
private String name;
private String email;
private Integer age;
private Integer orderCount; // 聚合字段
private BigDecimal totalAmount; // 聚合字段
private List<OrderVO> orders; // 嵌套列表
}
// Service 中组装
public List<UserVO> getUserVOs() {
List<User> users = userMapper.selectList(null);
List<Order> orders = orderMapper.selectList(null);
Map<Long, List<Order>> orderMap = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
return users.stream().map(user -> {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
List<Order> userOrders = orderMap.getOrDefault(user.getId(), Collections.emptyList());
vo.setOrderCount(userOrders.size());
vo.setOrders(userOrders.stream().map(o -> {
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(o, orderVO);
return orderVO;
}).collect(Collectors.toList()));
return vo;
}).collect(Collectors.toList());
}@Data
public class UserVO {
private Long id;
private String name;
private String email;
private Integer age;
private Integer orderCount; // 聚合字段
private BigDecimal totalAmount; // 聚合字段
private List<OrderVO> orders; // 嵌套列表
}
// Service 中组装
public List<UserVO> getUserVOs() {
List<User> users = userMapper.selectList(null);
List<Order> orders = orderMapper.selectList(null);
Map<Long, List<Order>> orderMap = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
return users.stream().map(user -> {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
List<Order> userOrders = orderMap.getOrDefault(user.getId(), Collections.emptyList());
vo.setOrderCount(userOrders.size());
vo.setOrders(userOrders.stream().map(o -> {
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(o, orderVO);
return orderVO;
}).collect(Collectors.toList()));
return vo;
}).collect(Collectors.toList());
}9.5 性能优化建议
1. 合理配置批量操作大小:
// 批量插入时合理设置批次大小
userService.saveBatch(userList, 1000); // 1000 条一批
// Spring JDBC 批量配置
spring.jdbc.template.fetch-size=1000// 批量插入时合理设置批次大小
userService.saveBatch(userList, 1000); // 1000 条一批
// Spring JDBC 批量配置
spring.jdbc.template.fetch-size=10002. 使用 selectObjs 只查单个字段:
// 只查 ID 字段,减少数据传输
List<Object> ids = userMapper.selectObjs(
Wrappers.<User>lambdaQuery().select(User::getId)
);// 只查 ID 字段,减少数据传输
List<Object> ids = userMapper.selectObjs(
Wrappers.<User>lambdaQuery().select(User::getId)
);3. 分页查询务必加条件:
// 好的做法:分页查询带上条件
Page<User> page = userMapper.selectPage(
new Page<>(1, 10),
Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime)
);
// 避免:无条件分页全表扫描
// userMapper.selectPage(new Page<>(1, 10), null); // 如果表数据量巨大// 好的做法:分页查询带上条件
Page<User> page = userMapper.selectPage(
new Page<>(1, 10),
Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime)
);
// 避免:无条件分页全表扫描
// userMapper.selectPage(new Page<>(1, 10), null); // 如果表数据量巨大4. 合理使用 Wrapper 的 select 方法指定字段:
// 只查询需要的字段,避免 SELECT *
userMapper.selectList(
Wrappers.<User>lambdaQuery()
.select(User::getId, User::getName, User::getEmail)
.eq(User::getStatus, 1)
);// 只查询需要的字段,避免 SELECT *
userMapper.selectList(
Wrappers.<User>lambdaQuery()
.select(User::getId, User::getName, User::getEmail)
.eq(User::getStatus, 1)
);5. 使用 @Transactional(readOnly = true) 优化查询:
@Service
public class QueryService {
@Transactional(readOnly = true)
public List<User> findAll() {
// 只读事务可以优化数据库连接(某些数据库)
return userMapper.selectList(null);
}
}@Service
public class QueryService {
@Transactional(readOnly = true)
public List<User> findAll() {
// 只读事务可以优化数据库连接(某些数据库)
return userMapper.selectList(null);
}
}6. 二级缓存策略:
- 读多写少的场景适合使用二级缓存
- 分布式环境下使用 Redis 作为二级缓存
- 注意缓存失效导致的数据不一致
7. SQL 性能分析:
# 开启慢 SQL 日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.example.mapper: debug# 开启慢 SQL 日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.example.mapper: debug8. 数据库索引优化:
-- 常用查询条件加索引
ALTER TABLE user ADD INDEX idx_status_name (status, name);
ALTER TABLE user ADD INDEX idx_create_time (create_time);
-- 分页用覆盖索引
ALTER TABLE user ADD INDEX idx_cover (status, id, name, email);-- 常用查询条件加索引
ALTER TABLE user ADD INDEX idx_status_name (status, name);
ALTER TABLE user ADD INDEX idx_create_time (create_time);
-- 分页用覆盖索引
ALTER TABLE user ADD INDEX idx_cover (status, id, name, email);9.6 常见问题与解决方案
问题 1:更新时部分字段被置为 null
// 问题:实体中有 null 字段,更新时会把数据库字段置为 null
User user = new User();
user.setId(1L);
user.setName("new name");
// email 字段为 null
userMapper.updateById(user);
// SQL: UPDATE user SET name='new name', email=NULL WHERE id=1// 问题:实体中有 null 字段,更新时会把数据库字段置为 null
User user = new User();
user.setId(1L);
user.setName("new name");
// email 字段为 null
userMapper.updateById(user);
// SQL: UPDATE user SET name='new name', email=NULL WHERE id=1// 方案一:使用 UpdateWrapper 选择性更新
userService.lambdaUpdate()
.set(User::getName, "new name")
.eq(User::getId, 1L)
.update();
// 方案二:配置字段更新策略
// application.yml
// mybatis-plus.global-config.db-config.update-strategy=not_null// 方案一:使用 UpdateWrapper 选择性更新
userService.lambdaUpdate()
.set(User::getName, "new name")
.eq(User::getId, 1L)
.update();
// 方案二:配置字段更新策略
// application.yml
// mybatis-plus.global-config.db-config.update-strategy=not_null问题 2:逻辑删除与唯一索引冲突
// 解决方案:联合唯一索引中包含 deleted 字段
// CREATE UNIQUE INDEX uk_name ON user(name, deleted);
// 但 deleted 值需使用时间戳或其他非固定值
// 或者使用不同的唯一键策略// 解决方案:联合唯一索引中包含 deleted 字段
// CREATE UNIQUE INDEX uk_name ON user(name, deleted);
// 但 deleted 值需使用时间戳或其他非固定值
// 或者使用不同的唯一键策略问题 3:MyBatis-Plus 和 MyBatis 原生配置冲突
# 正确方式:不要同时配置 mybatis 和 mybatis-plus 的 configuration
# 统一在 mybatis-plus.configuration 下配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true# 正确方式:不要同时配置 mybatis 和 mybatis-plus 的 configuration
# 统一在 mybatis-plus.configuration 下配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true问题 4:LambdaQueryWrapper 不支持泛型推断
// 错误:无法推断 User::getName
// LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// wrapper.eq(User::getName, "张三");
// 正确:使用 Wrappers 工具类
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三");// 错误:无法推断 User::getName
// LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// wrapper.eq(User::getName, "张三");
// 正确:使用 Wrappers 工具类
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三");10. 总结
MyBatis-Plus 是在 MyBatis 基础上发展的强大增强工具,通过非侵入式设计大幅提升了开发效率。核心要点:
- BaseMapper 提供单表 CRUD 零配置,IService 封装业务层常用操作
- 条件构造器 支持 Lambda 风格链式编程,避免硬编码字段名
- 分页插件 支持多种数据库,无需手写分页 SQL
- 代码生成器 一键生成全套代码,减少重复劳动
- 插件生态 提供乐观锁、逻辑删除、自动填充等实用功能
- 多数据源 支持读写分离和多库切换
- 复杂查询 仍然支持自定义 XML,不丢失 MyBatis 的灵活性
整体而言,MyBatis-Plus 极大地提高了使用 MyBatis 的开发效率,同时保留了 MyBatis 在 SQL 控制上的灵活性,是 Java 后端开发中的主流选择。
Cao Note