Skip to content

MyBatis / MyBatis-Plus 笔记 ​

1. MyBatis 概述 ​

1.1 ORM 概念 ​

ORM(Object Relational Mapping,对象关系映射)是一种将关系型数据库中的表结构映射为内存中对象的技术。通过 ORM,开发者可以使用面向对象的方式操作数据库,而不需要直接编写 SQL 语句。

ORM 框架特点优点缺点
MyBatis半自动化,SQL 由开发者编写灵活、SQL 可优化、学习成本低需要手写 SQL、无 Hibernate 透明
Hibernate全自动化,自动生成 SQL开发效率高、跨数据库SQL 难以优化、复杂查询性能差
JPAJava 标准规范,Hibernate 是实现标准化、与 Spring Data 集成好复杂场景不如 MyBatis 灵活
Spring JDBC TemplateJDBC 封装轻量、简单功能有限

1.2 MyBatis 与 Hibernate 对比 ​

对比维度MyBatisHibernate
自动化程度半自动(手写 SQL)全自动(自动生成 SQL)
SQL 优化完全可控需要了解 HQL 生成规则
学习曲线低高
性能调优直接优化 SQL需调整 Hibernate 配置
复杂查询灵活较复杂(需 JPQL/Criteria)
数据库依赖较强较弱(跨数据库友好)
缓存一级/二级缓存一级/二级/查询缓存
主流使用地区中国、日本欧美

1.3 为什么国内主流使用 MyBatis ​

  1. SQL 可控性强 — 国内互联网公司对 SQL 性能要求极高,DBA 可以直接审核和优化 SQL
  2. 复杂查询场景多 — 互联网业务报表、统计、多表关联查询频繁,MyBatis 更灵活
  3. 学习成本低 — 只要求熟悉 SQL,不要求深入理解 ORM 原理
  4. 历史原因 — 早年 Hibernate 版本配置繁琐,MyBatis(前身 iBatis)更轻量
  5. 与 Spring Boot 集成成熟 — mybatis-spring-boot-starter 使用广泛

2. MyBatis 核心配置 ​

2.1 mybatis-config.xml 配置 ​

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 依赖即可。

xml
<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 配置 ​

yaml
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: debug
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: debug

2.4 启动类与 Mapper 扫描 ​

java
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 属性一致。

java
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
<?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> 标签 ​

xml
<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> 标签 ​

xml
<!-- 返回自增主键 -->
<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> 标签 ​

xml
<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 注入安全有风险
适用场景传值(字段值)动态表名、列名、排序字段
性能高(可缓存执行计划)低
java
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);
}
xml
<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 注解详解:

java
// 不使用时,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>) ​

xml
<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 的继承:

xml
<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> 一对一关联 ​

java
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
}
xml
<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> 一对多关联 ​

java
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
}
xml
<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> 高级用法(延迟加载):

xml
<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> 标签 ​

xml
<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 &lt;= #{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 &lt;= #{maxAge}
    </if>
    <if test="email != null">
        AND email = #{email}
    </if>
</select>

注意:XML 中使用 < 和 > 需要转义,或者使用 CDATA:

xml
<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,只选取第一个匹配的条件。

xml
<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。

xml
<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> 等价于:

xml
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <!-- 动态条件 -->
</trim>
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <!-- 动态条件 -->
</trim>

4.4 <set> 标签 ​

<set> 标签用于 UPDATE 语句,动态生成 SET 子句并去除末尾逗号。

xml
<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> 等价于:

xml
<trim prefix="SET" suffixOverrides=",">
    <!-- 更新字段 -->
</trim>
<trim prefix="SET" suffixOverrides=",">
    <!-- 更新字段 -->
</trim>

4.5 <foreach> 标签 ​

<foreach> 用于遍历集合(List、Set、数组、Map),常用于 IN 查询和批量操作。

xml
<!-- 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 片段的前缀、后缀和覆盖规则。

xml
<!-- 自定义 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>
属性说明
prefixSQL 片段前添加的内容
suffixSQL 片段后添加的内容
prefixOverrides忽略前缀中的匹配字符
suffixOverrides忽略后缀中的匹配字符

4.7 <sql> 与 <include> 标签 ​

复用 SQL 片段的最佳实践。

xml
<!-- 定义 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 中创建变量,常用于模糊查询、字符串拼接。

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) ​

xml
<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 中配置:

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。

工作原理:

  1. 同一个 SqlSession 中执行相同查询
  2. MyBatis 会先检查缓存中是否存在结果
  3. 如果存在则直接返回缓存结果,不再查询数据库
  4. 如果执行了 INSERT / UPDATE / DELETE / commit / close / clearCache 等操作,缓存会被清空
java
@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。

开启方式:

xml
<!-- 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:

java
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;
    // ...
}

二级缓存使用示例:

java
@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 接口即可集成第三方缓存。

java
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):

java
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);
    }
}
xml
<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 ​

对比维度MyBatisMyBatis-Plus
单表 CRUD手写 SQL无需写 SQL,BaseMapper 内置
分页手写分页 SQL(limit/offset)分页插件,自动生成
条件查询手写动态 SQL条件构造器(Lambda 风格)
代码量多(XML + 接口)少(注解或零配置)
学习成本低非常低(熟悉 MyBatis 即可)
复杂查询自定义 SQL自定义 SQL + 扩展
更新频率较低活跃(社区生态好)

6.3 Spring Boot 配置 ​

添加依赖:

xml
<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 配置:

yaml
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: debug
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: debug

6.4 BaseMapper 接口 ​

创建实体类:

java
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):

java
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 内置方法大全:

java
// 插入
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);

使用示例:

java
@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 实现类,进一步封装业务层代码。

java
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);
}
java
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 内置方法一览:

java
// 插入
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 ​

java
// 基本用法
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 完整方法示例:

java
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 表达式,避免硬编码字段名(推荐使用)。

java
// 方式一:通过实体类获取 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 ​

java
// 方式一:直接使用实体 + 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 分页插件 ​

配置分页插件:

java
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;
    }
}

分页查询使用:

java
@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 分页方法:

java
// 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);
}
xml
<!-- 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(推荐,新版) ​

java
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(经典版) ​

java
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 乐观锁插件 ​

乐观锁用于解决并发更新时的数据冲突问题。

实现原理:

  1. 读取数据时获取版本号 version
  2. 更新数据时 SET version = version + 1
  3. 更新条件中带上原 version
  4. 如果 version 已被其他线程修改,则更新失败(影响行数为 0)

配置乐观锁插件:

java
@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;
    }
}

实体类配置:

java
@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;
}

使用示例:

java
@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 逻辑删除 ​

逻辑删除是在数据表中增加一个删除标记字段,删除操作实际为更新该字段。

配置逻辑删除:

yaml
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       # 未删除的值

实体类配置:

java
@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;
}

使用示例:

java
@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,自己拼完整条件
        );
    }
}

逻辑删除绕过方式:

java
// 方式一:使用原生 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:

java
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";
    }
}

实体类配置:

java
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 组件,支持多数据源和读写分离。

添加依赖:

xml
<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 配置:

yaml
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.Driver
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.Driver

使用 @DS 注解切换数据源:

java
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 绑定:

java
@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 其他插件 ​

防全表更新/删除插件 ​

java
@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;
}
java
// 当执行无条件的 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 性能分析插件 ​

java
@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;
}

动态表名插件 ​

java
@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 使用 ​

java
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:自调用(最常见)

java
@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 方法

java
@Transactional
private void doSave() {  // 事务不生效
    // ...
}
@Transactional
private void doSave() {  // 事务不生效
    // ...
}

场景 3:异常被 catch 吞掉

java
@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 没配对

java
@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 方法

java
@Service
public class OrderService {
    @Transactional
    public final void createOrder() {  // CGLIB 无法代理 final 方法
        // 事务不生效
    }
}
@Service
public class OrderService {
    @Transactional
    public final void createOrder() {  // CGLIB 无法代理 final 方法
        // 事务不生效
    }
}

场景 6:多线程

java
@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 处理分布式事务。

java
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.x2.7.x / 3.0.x3.5.xJava 8+
3.4.x2.3.x - 2.6.x3.5.xJava 8+
3.3.x2.1.x - 2.2.x3.4.xJava 8+

推荐的 Maven 依赖(Spring Boot 3.x + MyBatis-Plus 3.5.x):

xml
<!-- 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 次子查询。

问题示例:

java
// 错误写法:循环中执行查询
@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;
    }
}

解决方案:

java
// 方案一:使用 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();
}
xml
<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>
java
// 方案三:使用 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 的选择 ​

场景推荐方式原因
简单单表 CRUDIService内置 saveBatch、page 等方法,代码更少
复杂查询BaseMapper直接使用 Wrapper,更灵活
需要批量操作IService内置 saveBatch、updateBatchById
自定义 SQLBaseMapper + XML需要手写 SQL 时使用
事务性业务方法IService@Transactional + 链式调用更简洁
分页查询BaseMapper / IService二者都支持

混合使用示例:

java
@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 ​

java
@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(推荐) ​

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>
java
// 自定义 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)方式 ​

java
@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. 合理配置批量操作大小:

java
// 批量插入时合理设置批次大小
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=1000

2. 使用 selectObjs 只查单个字段:

java
// 只查 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. 分页查询务必加条件:

java
// 好的做法:分页查询带上条件
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 方法指定字段:

java
// 只查询需要的字段,避免 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) 优化查询:

java
@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 性能分析:

yaml
# 开启慢 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: debug

8. 数据库索引优化:

sql
-- 常用查询条件加索引
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

java
// 问题:实体中有 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
java
// 方案一:使用 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:逻辑删除与唯一索引冲突

java
// 解决方案:联合唯一索引中包含 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 原生配置冲突

yaml
# 正确方式:不要同时配置 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 不支持泛型推断

java
// 错误:无法推断 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 基础上发展的强大增强工具,通过非侵入式设计大幅提升了开发效率。核心要点:

  1. BaseMapper 提供单表 CRUD 零配置,IService 封装业务层常用操作
  2. 条件构造器 支持 Lambda 风格链式编程,避免硬编码字段名
  3. 分页插件 支持多种数据库,无需手写分页 SQL
  4. 代码生成器 一键生成全套代码,减少重复劳动
  5. 插件生态 提供乐观锁、逻辑删除、自动填充等实用功能
  6. 多数据源 支持读写分离和多库切换
  7. 复杂查询 仍然支持自定义 XML,不丢失 MyBatis 的灵活性

整体而言,MyBatis-Plus 极大地提高了使用 MyBatis 的开发效率,同时保留了 MyBatis 在 SQL 控制上的灵活性,是 Java 后端开发中的主流选择。