MyBatis 全套知识体系完全指南

发布于 2026-07-30 19:31 更新于 2026-07-30 19:31 13403 字 68 min read ... 访问量

本文全面系统地梳理了 MyBatis 的全套知识体系,从基础概念到高级实战,涵盖架构原理、核心配置、动态 SQL、关联查询、缓存机制、类型处理器、插件开发、注解使用、Spring 集成及 MyBatis-Plus 增强功能,并提供了常见问题排错与性能优化建议,适合初学者学习和资深开发者查阅参考。

MyBatis 全套知识体系完全指南

本文档全面覆盖 MyBatis 的理论知识与指令操作,从基础概念到高级实战,无遗漏。

MyBatis 概述

什么是 MyBatis

MyBatis 是一款 SQL 映射与持久层框架。它允许开发者自行编写 SQL,并负责参数绑定、结果映射、事务协作等重复工作。MyBatis 常被称为“半自动 ORM”,但它不会像全自动 ORM 框架那样根据实体关系自动生成全部 SQL。

MyBatis 前身是 iBatis,2010 年由 Apache 迁移到 Google Code 并更名为 MyBatis,后迁移至 GitHub。

MyBatis 的定位

特性JDBCMyBatisHibernate (JPA)
SQL 控制完全手动半自动(手写 SQL,自动映射)全自动(自动生成 SQL
学习成本高(大量样板代码)
灵活性最高较低
性能可控较难(需调优)
数据库移植性
适合场景复杂 SQL、高性能需求快速开发、简单 CRUD

MyBatis 的核心特性

 ┌────────────────────────────────────────────────────┐
 │              MyBatis 核心特性                        │
 ├──────────────┬──────────────┬─────────────────────┤
 │   SQL 映射    │  动态 SQL    │  高级映射            │
 ├──────────────┼──────────────┼─────────────────────┤
 │ XML/注解配置  │ 条件拼接      │ 一对一/一对多/多对多  │
 │ 参数自动绑定  │ 循环遍历      │ 嵌套结果/嵌套查询     │
 │ 结果自动映射  │ SQL 片段复用  │ 鉴别器(discriminator)│
 ├──────────────┼──────────────┼─────────────────────┤
│   缓存机制    │  插件机制     │  扩展能力            │
├──────────────┼──────────────┼─────────────────────┤
│ 一级缓存      │ 拦截器        │ TypeHandler          │
│ 二级缓存      │ 分页插件      │ ObjectFactory        │
│ 第三方缓存    │ SQL 审计      │ 语言驱动             │
└──────────────┴──────────────┴─────────────────────┘

MyBatis 架构

                     ┌─────────────────┐
                     │   应用程序层      │
                     │  (Service/DAO)   │
                     └────────┬────────┘

                     ┌────────▼────────┐
                     │  SqlSession 接口  │
                     │ (selectOne/list  │
                     │  insert/update   │
                    │  delete/commit)  │
                    └────────┬────────┘

                    ┌────────▼────────┐
                    │  Executor 执行器  │
                    │  (Simple/Reuse   │
                    │   Batch)         │
                    └────────┬────────┘

              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼───┐  ┌──────▼─────┐  ┌────▼────────┐
     │ Statement  │  │ Parameter  │  │  ResultSet  │
     │ Handler    │  │ Handler    │  │  Handler    │
     │(SQL预处理)  │  │(参数绑定)   │  │(结果映射)    │
     └────────┬───┘  └────────────┘  └─────────────┘

     ┌────────▼───┐
     │   JDBC     │
     │  数据库     │
     └────────────┘

MyBatis 版本

MyBatis 3 是目前课程和实际项目中常用的主版本系列。本文示例使用 3.5.15,版本号仅用于保证示例可复现;实际项目应以依赖管理中确定的版本为准,并在升级时查阅对应版本的发布说明。

快速入门

示例数据库

本文档统一使用以下示例数据库:

 -- 部门表
 CREATE TABLE dept (
     dept_id   INT PRIMARY KEY AUTO_INCREMENT,
     dept_name VARCHAR(50) NOT NULL,
     location  VARCHAR(100)
 );
 -- 员工表
 CREATE TABLE emp (
    emp_id     INT PRIMARY KEY AUTO_INCREMENT,
    emp_name   VARCHAR(50) NOT NULL,
    email      VARCHAR(100),
    salary     DECIMAL(10,2),
    dept_id    INT,
    hire_date  DATE,
    status     TINYINT DEFAULT 1,  -- 1:在职 0:离职
    FOREIGN KEY (dept_id) REFERENCES dept(dept_id)
);
-- 项目表(多对多)
CREATE TABLE project (
    project_id   INT PRIMARY KEY AUTO_INCREMENT,
    project_name VARCHAR(100) NOT NULL,
    budget       DECIMAL(12,2)
);
-- 员工-项目关联表(多对多)
CREATE TABLE emp_project (
    emp_id     INT,
    project_id INT,
    role       VARCHAR(50),
    PRIMARY KEY (emp_id, project_id),
    FOREIGN KEY (emp_id) REFERENCES emp(emp_id),
    FOREIGN KEY (project_id) REFERENCES project(project_id)
);

Maven 依赖

 <!-- MyBatis 核心 -->
 <dependency>
     <groupId>org.mybatis</groupId>
     <artifactId>mybatis</artifactId>
     <version>3.5.15</version>
 </dependency>
 <!-- MySQL 驱动 -->
 <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
</dependency>
<!-- 日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.12</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>
<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

实体类

 package com.example.entity;
 import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.List;
 @Data
 public class Emp {
    private Integer empId;
    private String empName;
    private String email;
    private BigDecimal salary;
    private Integer deptId;
    private LocalDate hireDate;
    private Integer status;
    // 关联对象
    private Dept dept;
    // 关联集合(多对多)
    private List<Project> projects;
}
@Data
public class Dept {
    private Integer deptId;
    private String deptName;
    private String location;
    // 关联集合(一对多)
    private List<Emp> emps;
}
@Data
public class Project {
    private Integer projectId;
    private String projectName;
    private BigDecimal budget;
    private List<Emp> emps;
}

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
     <!-- 属性配置 -->
     <properties resource="db.properties"/>
     <!-- 设置 -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="defaultExecutorType" value="REUSE"/>
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    <!-- 别名 -->
    <typeAliases>
        <package name="com.example.entity"/>
    </typeAliases>
    <!-- 环境配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 映射器 -->
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

db.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb?
    useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root123

Mapper 接口

 package com.example.mapper;
 import com.example.entity.Emp;
 import java.util.List;
 public interface EmpMapper {
     // 查询所有员工
     List<Emp> selectAll();
    // 根据 ID 查询
    Emp selectById(Integer empId);
    // 插入
    int insert(Emp emp);
    // 更新
    int update(Emp emp);
    // 删除
    int deleteById(Integer empId);
}

Mapper XML

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.example.mapper.EmpMapper">
     <select id="selectAll" resultType="Emp">
         SELECT * FROM emp
     </select>
    <select id="selectById" resultType="Emp" parameterType="int">
        SELECT * FROM emp WHERE emp_id = #{empId}
    </select>
    <insert id="insert" parameterType="Emp">
        INSERT INTO emp (emp_name, email, salary, dept_id, hire_date, status)
        VALUES (#{empName}, #{email}, #{salary}, #{deptId}, #{hireDate}, #{status})
    </insert>
    <update id="update" parameterType="Emp">
        UPDATE emp SET
            emp_name = #{empName},
            email = #{email},
            salary = #{salary},
            dept_id = #{deptId},
            status = #{status}
        WHERE emp_id = #{empId}
    </update>
    <delete id="deleteById" parameterType="int">
        DELETE FROM emp WHERE emp_id = #{empId}
    </delete>
</mapper>

工具类

 package com.example.util;
 import org.apache.ibatis.io.Resources;
 import org.apache.ibatis.session.SqlSession;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
 import java.io.InputStream;
 public class MyBatisUtil {
    private static final SqlSessionFactory sqlSessionFactory;
    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (Exception e) {
            throw new RuntimeException("MyBatis 初始化失败", e);
        }
    }
    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
    public static SqlSession getSqlSession(boolean autoCommit) {
        return sqlSessionFactory.openSession(autoCommit);
    }
}

测试

public class MyBatisTest {
    @Test
    public void testSelectAll() {
        try (SqlSession session = MyBatisUtil.getSqlSession()) {
            EmpMapper mapper = session.getMapper(EmpMapper.class);
             List<Emp> emps = mapper.selectAll();
             emps.forEach(System.out::println);
         }
    }
    @Test
    public void testInsert() {
        try (SqlSession session = MyBatisUtil.getSqlSession()) {
            EmpMapper mapper = session.getMapper(EmpMapper.class);
            Emp emp = new Emp();
            emp.setEmpName("张三");
            emp.setEmail("zhangsan@example.com");
            emp.setSalary(new BigDecimal("15000"));
            emp.setDeptId(1);
            emp.setHireDate(LocalDate.now());
            emp.setStatus(1);
            int rows = mapper.insert(emp);
            session.commit();  // 必须手动提交
            System.out.println("插入行数: " + rows + ", 主键: " + emp.getEmpId());
        }
    }
}

核心配置文件(mybatis-config.xml)

完整配置结构

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
         "https://mybatis.org/dtd/mybatis-3-config.dtd">
 <configuration>
     <!-- 1. 属性:引入外部 properties 文件 -->
     <properties resource="db.properties">
         <!-- 也可以在这里定义默认属性 -->
         <property name="username" value="root"/>
    </properties>
    <!-- 2. 设置:全局行为配置 -->
    <settings>
        <!-- 驼峰命名自动映射(emp_name → empName) -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 二级缓存开关 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 延迟加载开关 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 是否按需加载(3.4.2+默认true) -->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 延迟加载触发方法 -->
        <setting name="lazyLoadTriggerMethods"
     value="equals,clone,hashCode,toString"/>
        <!-- 默认执行器类型 -->
        <setting name="defaultExecutorType" value="REUSE"/>
        <!-- 默认语句超时(秒) -->
        <setting name="defaultStatementTimeout" value="30"/>
        <!-- 默认获取大小 -->
        <setting name="defaultFetchSize" value="100"/>
        <!-- 本地缓存范围 -->
        <setting name="localCacheScope" value="SESSION"/>
        <!-- 默认事务隔离级别 -->
        <setting name="defaultTransactionIsolationLevel" value="READ_COMMITTED"/>
        <!-- 日志实现 -->
        <setting name="logImpl" value="SLF4J"/>
        <!-- 代理工厂(CGLIB/JAVASSIST) -->
        <setting name="proxyFactory" value="JAVASSIST"/>
        <!-- 多结果集 -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        <!-- 列名标签前缀 -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 使用生成的主键 -->
        <setting name="useGeneratedKeys" value="false"/>
        <!-- 自动映射行为 -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <!-- 自动映射未知列行为 -->
        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
        <!-- 返回行数上限 -->
        <setting name="safeRowBoundsEnabled" value="false"/>
        <!-- 安全的 ResultHandler -->
        <setting name="safeResultHandlerEnabled" value="true"/>
        <!-- 指定 MyBatis 语言驱动 -->
        <setting name="defaultScriptingLanguage"
     value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
        <!-- 枚举类型默认处理器 -->
        <setting name="defaultEnumTypeHandler"
     value="org.apache.ibatis.type.EnumTypeHandler"/>
        <!-- NULL 值的 JDBC 类型 -->
        <setting name="jdbcTypeForNull" value="OTHER"/>
    </settings>
    <!-- 3. 类型别名 -->
    <typeAliases>
        <!-- 单个类配置 -->
        <typeAlias type="com.example.entity.Emp" alias="Emp"/>
        <!-- 包扫描(默认用类名首字母小写) -->
        <package name="com.example.entity"/>
    </typeAliases>
    <!-- 4. 类型处理器 -->
    <typeHandlers>
        <typeHandler handler="com.example.handler.StringListTypeHandler"/>
        <package name="com.example.handler"/>
     </typeHandlers>
     <!-- 5. 对象工厂 -->
     <objectFactory type="com.example.factory.CustomObjectFactory">
         <property name="someProperty" value="100"/>
     </objectFactory>
     <!-- 6. 插件(拦截器) -->
     <plugins>
         <plugin interceptor="com.example.plugin.MyPagePlugin">
             <property name="dialect" value="mysql"/>
         </plugin>
     </plugins>
     <!-- 7. 环境配置 -->
     <environments default="development">
         <environment id="development">
             <transactionManager type="JDBC"/>
             <dataSource type="POOLED">
                 <property name="driver" value="${jdbc.driver}"/>
                 <property name="url" value="${jdbc.url}"/>
                 <property name="username" value="${jdbc.username}"/>
                 <property name="password" value="${jdbc.password}"/>
                 <!-- 连接池配置 -->
                 <property name="poolMaximumActiveConnections" value="10"/>
                 <property name="poolMaximumIdleConnections" value="5"/>
                 <property name="poolMaximumCheckoutTime" value="20000"/>
                 <property name="poolTimeToWait" value="20000"/>
                <property name="poolPingEnabled" value="true"/>
                <property name="poolPingQuery" value="SELECT 1"/>
                <property name="poolPingConnectionsNotUsedFor" value="3600000"/>
            </dataSource>
        </environment>
        <environment id="production">
            <transactionManager type="MANAGED"/>
            <dataSource type="JNDI">
                <property name="initialContext" value="java:comp/env"/>
                <property name="dataSource" value="jdbc/myDataSource"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 8. 数据库厂商标识 -->
    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="Oracle" value="oracle"/>
        <property name="PostgreSQL" value="pg"/>
    </databaseIdProvider>
    <!-- 9. 映射器 -->
    <mappers>
        <!-- 方式一:资源路径 -->
        <mapper resource="mapper/EmpMapper.xml"/>
        <!-- 方式二:URL -->
        <mapper url="file:///var/mappers/EmpMapper.xml"/>
        <!-- 方式三:接口类 -->
        <mapper class="com.example.mapper.EmpMapper"/>
        <!-- 方式四:包扫描 -->
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

配置元素顺序

mybatis-config.xml 的顶层元素必须按下列顺序出现。没有使用的元素可以省略,但不能随意交换顺序。

1. properties
2. settings
3. typeAliases
4. typeHandlers
5. objectFactory
6. objectWrapperFactory
7. reflectorFactory
8. plugins
9. environments
10. databaseIdProvider
11. mappers

常用 Settings 详解

设置项可选值默认值说明
cacheEnabledtruefalsetrue是否全局启用映射器二级缓存
lazyLoadingEnabledtruefalsefalse是否启用延迟加载
aggressiveLazyLoadingtruefalsefalse是否由任意方法调用触发对象的全部延迟属性加载
mapUnderscoreToCamelCasetruefalsefalse是否将下划线命名自动映射为驼峰命名
useGeneratedKeystruefalsefalse是否允许 JDBC 使用自动生成主键
defaultExecutorTypeSIMPLEREUSEBATCHSIMPLE默认执行器类型
defaultStatementTimeout正整数未设置SQL 执行超时时间,单位为秒
localCacheScopeSESSIONSTATEMENTSESSION一级缓存作用域
jdbcTypeForNullJDBC 类型OTHERnull 参数指定 JDBC 类型
logImpl日志实现类自动检测指定 MyBatis 使用的日志实现
autoMappingBehaviorNONEPARTIALFULLPARTIAL自动映射级别

三种数据源类型

type说明适用场景
UNPOOLED每次请求打开/关闭连接简单应用
POOLED连接池大多数应用(推荐)
JNDI从 JNDI 获取数据源应用服务器(Tomcat 等)

两种事务管理器

type说明适用场景
JDBC直接使用 JDBC 的 commit/rollback独立应用
MANAGED容器管理事务(不提交/回滚)应用服务器、Spring

类型别名

MyBatis 内置的常用别名:

别名Java 类型
_intint
intInteger
_longlong
longLong
_booleanboolean
booleanBoolean
stringString
integerInteger
dateDate
decimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList

SqlSession 与映射器接口

SqlSession 生命周期

 SqlSessionFactoryBuilder  →  一次创建后丢弃(方法局部变量)


 SqlSessionFactory          →  应用级别(单例,全局唯一)


 SqlSession                →  方法/请求级别(非线程安全)


Mapper 接口实例            →  SqlSession 级别

SqlSession 核心方法

 // 查询
 <T> T selectOne(String statement);
 <T> T selectOne(String statement, Object parameter);
 <E> List<E> selectList(String statement);
 <E> List<E> selectList(String statement, Object parameter);
 <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
 <K, V> Map<K, V> selectMap(String statement, String mapKey);
 <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
// 游标查询
<T> Cursor<T> selectCursor(String statement);
// 插入
int insert(String statement);
int insert(String statement, Object parameter);
// 更新
int update(String statement);
int update(String statement, Object parameter);
// 删除
int delete(String statement);
int delete(String statement, Object parameter);
// 事务
void commit();
void commit(boolean force);
void rollback();
void rollback(boolean force);
// 批量
void flushStatements();
// 关闭
void close();
// 清除缓存
void clearCache();
// 获取配置
Configuration getConfiguration();
// 获取映射器
<T> T getMapper(Class<T> type);
// 获取连接
Connection getConnection();

两种使用方式

方式一:通过命名空间直接调用

SqlSession session = MyBatisUtil.getSqlSession();
List<Emp> emps = session.selectList("com.example.mapper.EmpMapper.selectAll");
session.close();

方式二:通过 Mapper 接口调用(推荐)

SqlSession session = MyBatisUtil.getSqlSession();
EmpMapper mapper = session.getMapper(EmpMapper.class);
List<Emp> emps = mapper.selectAll();
session.close();

推荐方式二:类型安全、IDE 提示、编译期检查。

三种执行器

执行器说明适用场景
Simple每次执行创建新 Statement默认
Reuse复用 Statement多次相同 SQL
Batch批量执行批量 INSERT/UPDATE
// 指定执行器
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

事务管理

 // 手动提交(默认)
 SqlSession session = sqlSessionFactory.openSession();  // autoCommit = false
 try {
     EmpMapper mapper = session.getMapper(EmpMapper.class);
     mapper.insert(emp);
     session.commit();   // 手动提交
 } catch (Exception e) {
     session.rollback(); // 回滚
 } finally {
    session.close();
}
// 自动提交
SqlSession session = sqlSessionFactory.openSession(true);  // autoCommit = true
// try-with-resources(推荐)
try (SqlSession session = sqlSessionFactory.openSession()) {
    EmpMapper mapper = session.getMapper(EmpMapper.class);
    mapper.insert(emp);
    session.commit();
}

映射器文件(Mapper XML)

完整结构

 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <!-- namespace 必须与 Mapper 接口全限定名一致 -->
 <mapper namespace="com.example.mapper.EmpMapper">
     <!-- ============ 1. 缓存配置 ============ -->
     <cache/>                    <!-- 开启二级缓存(默认) -->
    <cache-ref namespace="com.example.mapper.DeptMapper"/>  <!-- 引用其他命名空间的缓存 -->
    <!-- ============ 2. 结果映射 ============ -->
    <resultMap id="empResultMap" type="Emp">
        <id property="empId" column="emp_id"/>
        <result property="empName" column="emp_name"/>
        <result property="email" column="email"/>
    </resultMap>
    <!-- ============ 3. SQL 片段 ============ -->
    <sql id="empColumns">
        emp_id, emp_name, email, salary, dept_id, hire_date, status
    </sql>
    <sql id="empWhere">
        <where>
            <if test="empName != null and empName != ''">
                AND emp_name LIKE CONCAT('%', #{empName}, '%')
            </if>
            <if test="deptId != null">
                AND dept_id = #{deptId}
            </if>
        </where>
    </sql>
    <!-- ============ 4. 查询 ============ -->
    <select id="selectAll" resultType="Emp">
        SELECT <include refid="empColumns"/> FROM emp
    </select>
    <!-- ============ 5. 插入 ============ -->
    <insert id="insert" parameterType="Emp"
            useGeneratedKeys="true" keyProperty="empId">
        INSERT INTO emp (emp_name, email, salary, dept_id, hire_date, status)
        VALUES (#{empName}, #{email}, #{salary}, #{deptId}, #{hireDate}, #{status})
    </insert>
    <!-- ============ 6. 更新 ============ -->
    <update id="update" parameterType="Emp">
        UPDATE emp SET
            emp_name = #{empName},
            salary = #{salary}
        WHERE emp_id = #{empId}
    </update>
    <!-- ============ 7. 删除 ============ -->
    <delete id="deleteById" parameterType="int">
        DELETE FROM emp WHERE emp_id = #{empId}
    </delete>
</mapper>

select 元素属性详解

 <select
     id="selectEmp"              <!-- 必须与接口方法名一致 -->
     parameterType="map"         <!-- 参数类型(可省略,自动推断) -->
     resultType="Emp"            <!-- 返回类型 -->
     resultMap="empResultMap"    <!-- 结果映射(与 resultType 二选一) -->
     flushCache="false"          <!-- 是否清空缓存 -->
     useCache="true"             <!-- 是否使用二级缓存 -->
     timeout="30"                <!-- 超时秒数 -->
     fetchSize="100"             <!-- 批量获取大小 -->
    statementType="PREPARED"    <!-- STATEMENT/PREPARED/CALLABLE -->
    resultSetType="FORWARD_ONLY" <!-- FORWARD_ONLY/SCROLL_INSENSITIVE/SCROLL_SENSITIVE
     -->
    databaseId="mysql"          <!-- 数据库厂商标识 -->
    resultOrdered="false"       <!-- 嵌套结果是否按顺序 -->
    resultSets="emp,dept"       <!-- 多结果集名 -->
    >
    SELECT * FROM emp WHERE emp_id = #{empId}
</select>

#{} 与 ${} 的区别

特性#{}${}
处理方式预编译参数(PreparedStatement 的 ?)字符串拼接
SQL 注入✅ 防止❌ 有风险
性能可利用预编译缓存每次都不同
使用场景参数值表名**/列名/ORDER BY**
 <!-- #{}:安全,用于传值 -->
 <select id="selectById" resultType="Emp">
     SELECT * FROM emp WHERE emp_id = #{empId}  <!-- → WHERE emp_id = ? -->
 </select>
 <!-- ${}:不安全,用于动态表名/列名 -->
 <select id="selectByColumn" resultType="Emp">
     SELECT * FROM emp ORDER BY ${columnName} ${order}
     <!-- → SELECT * FROM emp ORDER BY emp_id DESC -->
</select>
<!-- 必须对 ${} 的输入做校验 -->

参数传递

单个参数

// 接口
Emp selectById(Integer empId);
<!-- XML:#{任意名} -->
<select id="selectById" resultType="Emp">
    SELECT * FROM emp WHERE emp_id = #{empId}
    <!-- 也可写成 #{id} 或 #{任意名},单参数不限制 -->
</select>

多个参数(使用 @Param)

// 接口
List<Emp> selectByCondition(@Param("empName") String empName,
                             @Param("deptId") Integer deptId);
<select id="selectByCondition" resultType="Emp">
    SELECT * FROM emp
    WHERE emp_name LIKE CONCAT('%', #{empName}, '%')
      AND dept_id = #{deptId}
</select>

多个参数(使用 Map / 对象)

// Map
List<Emp> selectByMap(Map<String, Object> params);
// 对象
List<Emp> selectByEmp(Emp query);
 <!-- Map:#{key} -->
 <select id="selectByMap" resultType="Emp">
     SELECT * FROM emp
     WHERE emp_name = #{name} AND dept_id = #{deptId}
 </select>
 <!-- 对象:#{属性名} -->
 <select id="selectByEmp" resultType="Emp" parameterType="Emp">
     SELECT * FROM emp
    WHERE emp_name = #{empName} AND dept_id = #{deptId}
</select>

参数为 List / Array

1List<Emp> selectByIds(@Param(“ids”) List<Integer> ids);

<select id="selectByIds" resultType="Emp">
    SELECT * FROM emp WHERE emp_id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

insert 元素详解

 <!-- 普通插入 -->
 <insert id="insert" parameterType="Emp">
     INSERT INTO emp (emp_name, email, salary)
     VALUES (#{empName}, #{email}, #{salary})
 </insert>
 <!-- 主键回填(自增主键) -->
 <insert id="insert" parameterType="Emp"
         useGeneratedKeys="true" keyProperty="empId" keyColumn="emp_id">
    INSERT INTO emp (emp_name, email)
    VALUES (#{empName}, #{email})
</insert>
<!-- 插入后 emp.getEmpId() 可获取生成的主键 -->
<!-- 主键回填(非自增主键,如 Oracle 序列) -->
<insert id="insert" parameterType="Emp">
    <selectKey keyProperty="empId" resultType="int" order="BEFORE">
        SELECT SEQ_EMP.NEXTVAL FROM DUAL
    </selectKey>
    INSERT INTO emp (emp_id, emp_name)
    VALUES (#{empId}, #{empName})
</insert>
<!-- MySQL/PostgreSQL 使用 UUID -->
<insert id="insert" parameterType="Emp">
    <selectKey keyProperty="empId" resultType="string" order="BEFORE">
        SELECT UUID()
    </selectKey>
    INSERT INTO emp (emp_id, emp_name)
    VALUES (#{empId}, #{empName})
</insert>
selectKey 属性说明
keyProperty主键值要写入的 Java 属性
keyColumn数据库中的主键列名,多列主键时可指定
resultType主键值的 Java 类型
orderBEFORE 表示插入前查询主键,AFTER 表示插入后查询主键

SQL 片段

<!-- 定义 SQL 片段 -->
<sql id="empColumns">
    emp_id, emp_name, email, salary, dept_id, hire_date, status
</sql>
 <sql id="empColumnsWithAlias">
     e.emp_id, e.emp_name, e.email, e.salary, e.dept_id, e.hire_date, e.status
 </sql>
<!-- 引用 SQL 片段 -->
<select id="selectAll" resultType="Emp">
    SELECT <include refid="empColumns"/> FROM emp
</select>
<!-- 带参数的 SQL 片段 -->
<sql id="tableName">
    ${prefix}_emp
</sql>
<select id="selectAll" resultType="Emp">
    SELECT * FROM <include refid="tableName">
        <property name="prefix" value="t"/>
    </include>
</select>

ResultMap 详解

为什么需要 ResultMap

当数据库列名与 Java 属性名不一致时,需要通过 ResultMap 进行映射:

数据库列名: emp_id, emp_name, hire_date
Java 属性名: empId, empName, hireDate
→ 开启 mapUnderscoreToCamelCase 可自动映射
→ 但复杂映射(关联、集合、鉴别器)必须用 ResultMap

ResultMap 基本结构

 <resultMap id="empResultMap" type="Emp">
     <!-- id:主键映射(MyBatis 用它判断对象唯一性) -->
     <id property="empId" column="emp_id"/>
     <!-- result:普通字段映射 -->
     <result property="empName" column="emp_name"/>
     <result property="email" column="email"/>
     <result property="salary" column="salary"/>
     <result property="hireDate" column="hire_date"/>
</resultMap>

constructor 构造器映射

 // 实体类有带参构造器
 public class Emp {
     private Integer empId;
     private String empName;
     public Emp(Integer empId, String empName) {
         this.empId = empId;
         this.empName = empName;
     }
}
<resultMap id="empResultMap" type="Emp">
    <constructor>
        <idArg javaType="int" column="emp_id"/>
        <arg javaType="string" column="emp_name"/>
    </constructor>
</resultMap>

autoMapping 自动映射

 <!-- 全局设置 -->
 <settings>
     <!-- NONE: 禁用自动映射 -->
     <!-- PARTIAL(默认): 自动映射非嵌套结果 -->
     <!-- FULL: 自动映射所有(含嵌套) -->
     <setting name="autoMappingBehavior" value="PARTIAL"/>
 </settings>
 <!-- ResultMap 级别 -->
<resultMap id="empResultMap" type="Emp" autoMapping="true">
    <id property="empId" column="emp_id"/>
    <result property="empName" column="emp_name"/>
    <!-- 其他字段自动映射 -->
</resultMap>

ResultMap 继承

 <!-- 基础 ResultMap -->
 <resultMap id="baseResultMap" type="Emp">
     <id property="empId" column="emp_id"/>
     <result property="empName" column="emp_name"/>
     <result property="email" column="email"/>
     <result property="salary" column="salary"/>
 </resultMap>
 <!-- 继承并扩展 -->
<resultMap id="empWithDeptResultMap" type="Emp" extends="baseResultMap">
    <association property="dept" javaType="Dept">
        <id property="deptId" column="d_dept_id"/>
        <result property="deptName" column="dept_name"/>
    </association>
</resultMap>

鉴别器(Discriminator)

根据某列的值决定使用不同的 ResultMap:

 <resultMap id="employeeResultMap" type="Emp">
     <id property="empId" column="emp_id"/>
     <result property="empName" column="emp_name"/>
     <!-- 鉴别器:根据 status 的值决定映射方式 -->
     <discriminator javaType="int" column="status">
         <!-- status = 1:在职员工,映射部门 -->
         <case value="1" resultType="Emp">
             <association property="dept" javaType="Dept">
                <id property="deptId" column="dept_id"/>
                <result property="deptName" column="dept_name"/>
            </association>
        </case>
        <!-- status = 0:离职员工,映射离职信息 -->
        <case value="0" resultType="Emp" resultMap="resignedEmpResultMap"/>
    </discriminator>
</resultMap>

动态 SQL

动态 SQL 元素总览

元素说明类似于
<if>条件判断if
<choose>/<when>/<otherwise>多条件选择switch/case/default
<where>智能 WHERE 子句
<set>智能 SET 子句
<trim>自定义前缀/后缀裁剪
<foreach>遍历集合for
<bind>绑定变量
<sql>/<include>SQL 片段复用

if 元素

 <select id="selectByCondition" resultType="Emp">
     SELECT * FROM emp
     WHERE 1=1
     <if test="empName != null and empName != ''">
         AND emp_name LIKE CONCAT('%', #{empName}, '%')
     </if>
     <if test="deptId != null">
         AND dept_id = #{deptId}
     </if>
    <if test="minSalary != null">
        AND salary &gt;= #{minSalary}
    </if>
    <if test="maxSalary != null">
        AND salary &lt;= #{maxSalary}
    </if>
    <if test="status != null">
        AND status = #{status}
    </if>
</select>

注意:XML 中 < 和 > 需要转义为 &lt; 和 &gt;,或使用 <![CDATA[…]]> 包裹。

where 元素

<where> 会自动处理:

  1. 去掉首个 AND 或 OR
  2. 如果没有任何条件,则不加 WHERE
 <select id="selectByCondition" resultType="Emp">
     SELECT * FROM emp
     <where>
         <if test="empName != null and empName != ''">
             AND emp_name LIKE CONCAT('%', #{empName}, '%')
         </if>
         <if test="deptId != null">
             AND dept_id = #{deptId}
         </if>
        <if test="minSalary != null">
            AND salary &gt;= #{minSalary}
        </if>
    </where>
</select>

choose / when / otherwise

 <select id="selectByChoice" resultType="Emp">
     SELECT * FROM emp
     <where>
         <choose>
             <when test="empName != null and empName != ''">
                 emp_name = #{empName}
             </when>
             <when test="email != null and email != ''">
                 email = #{email}
            </when>
            <otherwise>
                status = 1
            </otherwise>
        </choose>
    </where>
</select>

set 元素

<set> 会自动处理:

  1. 去掉末尾多余的逗号
  2. 如果没有任何条件,则不加 SET(会报错)
 <update id="updateSelective" parameterType="Emp">
     UPDATE emp
     <set>
         <if test="empName != null">emp_name = #{empName},</if>
         <if test="email != null">email = #{email},</if>
         <if test="salary != null">salary = #{salary},</if>
         <if test="deptId != null">dept_id = #{deptId},</if>
         <if test="status != null">status = #{status},</if>
     </set>
    WHERE emp_id = #{empId}
</update>

trim 元素

<trim><where><set> 等元素的基础能力之一,可以自定义前缀、后缀以及需要移除的内容。

<!-- 等价于 <where> 的常见写法 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
</trim>

<!-- 等价于 <set> 的常见写法 -->
<trim prefix="SET" suffixOverrides=",">
    ...
</trim>
属性说明
prefix在最终内容前添加前缀
suffix在最终内容后添加后缀
prefixOverrides移除内容开头匹配的字符串
suffixOverrides移除内容末尾匹配的字符串

foreach 元素

 <!-- IN 查询 -->
 <select id="selectByIds" resultType="Emp">
     SELECT * FROM emp WHERE emp_id IN
     <foreach collection="ids" item="id" open="(" separator="," close=")">
         #{id}
     </foreach>
 </select>
 <!-- 生成:SELECT * FROM emp WHERE emp_id IN (1, 2, 3) -->
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list">
    INSERT INTO emp (emp_name, email, salary)
    VALUES
    <foreach collection="list" item="emp" separator=",">
        (#{emp.empName}, #{emp.email}, #{emp.salary})
    </foreach>
</insert>
<!-- 生成:INSERT INTO emp ... VALUES ('张三','a@b.com',10000),('李四','c@d.com',12000) -
     ->
<!-- 批量更新(CASE WHEN 方式) -->
<update id="batchUpdate" parameterType="list">
    UPDATE emp SET
    salary =
    <foreach collection="list" item="emp" open="CASE emp_id" close="END">
        WHEN #{emp.empId} THEN #{emp.salary}
    </foreach>
    WHERE emp_id IN
    <foreach collection="list" item="emp" open="(" separator="," close=")">
        #{emp.empId}
    </foreach>
</update>
<!-- OR 条件 -->
<select id="selectByNames" resultType="Emp">
    SELECT * FROM emp WHERE
    <foreach collection="names" item="name" separator=" OR ">
        emp_name = #{name}
    </foreach>
</select>
属性说明
collection要遍历的集合名称。单个 List 参数通常为 list,数组通常为 array,也可以使用 @Param 指定名称
item当前元素的变量名
index当前索引或 Map 的键
open整段内容的起始字符串
close整段内容的结束字符串
separator相邻元素之间的分隔符

bind 元素

 <!-- 模糊查询拼接 -->
 <select id="selectByName" resultType="Emp">
     <bind name="likeName" value="'%' + empName + '%'" />
     SELECT * FROM emp WHERE emp_name LIKE #{likeName}
 </select>
 <!-- 绑定多个变量 -->
 <select id="selectByCondition" resultType="Emp">
     <bind name="minSalaryValue" value="minSalary != null ? minSalary : 0" />
    <bind name="maxSalaryValue" value="maxSalary != null ? maxSalary : 999999" />
    SELECT * FROM emp
    WHERE salary BETWEEN #{minSalaryValue} AND #{maxSalaryValue}
</select>

完整动态 SQL 示例

<select id="selectByDynamicCondition" resultType="Emp">
    SELECT
    <include refid="empColumns"/>
    FROM emp
    <where>
        <if test="empName != null and empName != ''">
            <bind name="likeName" value="'%' + empName + '%'"/>
            AND emp_name LIKE #{likeName}
         </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
        <if test="deptId != null">
            AND dept_id = #{deptId}
        </if>
        <if test="deptIds != null and deptIds.size() > 0">
            AND dept_id IN
            <foreach collection="deptIds" item="did" open="(" separator="," close=")">
                #{did}
            </foreach>
        </if>
        <if test="minSalary != null or maxSalary != null">
            <choose>
                <when test="minSalary != null and maxSalary != null">
                    AND salary BETWEEN #{minSalary} AND #{maxSalary}
                </when>
                <when test="minSalary != null">
                    AND salary &gt;= #{minSalary}
                </when>
                <otherwise>
                    AND salary &lt;= #{maxSalary}
                </otherwise>
            </choose>
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
    ORDER BY emp_id DESC
</select>

关联查询

一对一关联(association)

方式一:嵌套结果(join 查询,一次 SQL)

public class Emp {
    private Integer empId;
    private String empName;
    private Dept dept;  // 关联的部门对象
}
<resultMap id="empWithDeptResultMap" type="Emp">
    <id property="empId" column="emp_id"/>
    <result property="empName" column="emp_name"/>
    <result property="email" column="email"/>
     <!-- 一对一关联 -->
     <association property="dept" javaType="Dept">
         <id property="deptId" column="d_dept_id"/>
         <result property="deptName" column="d_dept_name"/>
        <result property="location" column="d_location"/>
    </association>
</resultMap>
<select id="selectEmpWithDept" resultMap="empWithDeptResultMap">
    SELECT
        e.emp_id,
        e.emp_name,
        e.email,
        d.dept_id   AS d_dept_id,
        d.dept_name AS d_dept_name,
        d.location  AS d_location
    FROM emp e
    LEFT JOIN dept d ON e.dept_id = d.dept_id
    WHERE e.emp_id = #{empId}
</select>

方式二:嵌套查询(分步查询,N+1 问题)

 <resultMap id="empWithDeptStepResultMap" type="Emp">
     <id property="empId" column="emp_id"/>
     <result property="empName" column="emp_name"/>
     <!-- column: 传递给子查询的列名; select: 子查询的方法全限定名 -->
     <association property="dept"
                  javaType="Dept"
                  column="dept_id"
                  select="com.example.mapper.DeptMapper.selectById"
                 fetchType="lazy"/>  <!-- lazy: 延迟加载; eager: 立即加载 -->
</resultMap>
<select id="selectEmpWithDeptStep" resultMap="empWithDeptStepResultMap">
    SELECT * FROM emp WHERE emp_id = #{empId}
</select>
<!-- DeptMapper.xml -->
<select id="selectById" resultType="Dept">
    SELECT * FROM dept WHERE dept_id = #{deptId}
</select>

执行流程:

  1. 先执行 SELECT * FROM emp WHERE emp_id = ?
  2. 取出 dept_id,执行 SELECT * FROM dept WHERE dept_id = ?

方式三:嵌套查询传多个参数

<association property="dept"
             javaType="Dept"
             column="{deptId=dept_id, status=status}"
             select="com.example.mapper.DeptMapper.selectByIdAndStatus"
             fetchType="lazy"/>

一对多关联(collection)

public class Dept {
    private Integer deptId;
    private String deptName;
    private List<Emp> emps;  // 关联的员工列表
}

方式一:嵌套结果

 <resultMap id="deptWithEmpsResultMap" type="Dept">
     <id property="deptId" column="dept_id"/>
     <result property="deptName" column="dept_name"/>
     <result property="location" column="location"/>
     <!-- 一对多关联 -->
     <collection property="emps" ofType="Emp">
         <id property="empId" column="e_emp_id"/>
         <result property="empName" column="e_emp_name"/>
        <result property="email" column="e_email"/>
        <result property="salary" column="e_salary"/>
    </collection>
</resultMap>
<select id="selectDeptWithEmps" resultMap="deptWithEmpsResultMap">
    SELECT
        d.dept_id,
        d.dept_name,
        d.location,
        e.emp_id   AS e_emp_id,
        e.emp_name AS e_emp_name,
        e.email    AS e_email,
        e.salary   AS e_salary
    FROM dept d
    LEFT JOIN emp e ON d.dept_id = e.dept_id
    WHERE d.dept_id = #{deptId}
</select>

方式二:嵌套查询

 <resultMap id="deptWithEmpsStepResultMap" type="Dept">
     <id property="deptId" column="dept_id"/>
     <result property="deptName" column="dept_name"/>
     <collection property="emps"
                 ofType="Emp"
                 column="dept_id"
                 select="com.example.mapper.EmpMapper.selectByDeptId"
                 fetchType="lazy"/>
</resultMap>
<select id="selectDeptWithEmpsStep" resultMap="deptWithEmpsStepResultMap">
    SELECT * FROM dept WHERE dept_id = #{deptId}
</select>

多对多关联

public class Emp {
    private Integer empId;
    private String empName;
    private List<Project> projects;  // 参与的项目
}
 <resultMap id="empWithProjectsResultMap" type="Emp">
     <id property="empId" column="emp_id"/>
     <result property="empName" column="emp_name"/>
     <collection property="projects" ofType="Project">
         <id property="projectId" column="p_project_id"/>
         <result property="projectName" column="p_project_name"/>
         <result property="budget" column="p_budget"/>
     </collection>
</resultMap>
<select id="selectEmpWithProjects" resultMap="empWithProjectsResultMap">
    SELECT
        e.emp_id,
        e.emp_name,
        p.project_id   AS p_project_id,
        p.project_name AS p_project_name,
        p.budget       AS p_budget
    FROM emp e
    LEFT JOIN emp_project ep ON e.emp_id = ep.emp_id
    LEFT JOIN project p ON ep.project_id = p.project_id
    WHERE e.emp_id = #{empId}
</select>

嵌套查询 vs 嵌套结果对比

特性嵌套结果(join)嵌套查询(分步**)**
SQL 次数1 次(多表 JOIN**)**N+1 次
性能高(单次查询**)**低(N+1 问题,但可延迟加载**)**
延迟加载❌ 不支持✅ 支持
SQL 复杂度高(多表 JOIN**)**低(简单查询**)**
数据量大(笛卡尔积**)**分步获取
推荐关联数据少关联数据多 + 延迟加载

延迟加载配置

<!-- mybatis-config.xml -->
<settings>
    <!-- 全局开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- false: 按需加载(仅访问关联属性时才加载) -->
    <setting name="aggressiveLazyLoading" value="false"/>
    <!-- 延迟加载触发方法 -->
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<!-- 单个 association/collection 级别 -->
<association property="dept" fetchType="lazy" .../>
<collection property="emps" fetchType="eager" .../>

缓存机制

缓存体系

 ┌──────────────────────────────────────────────┐
 │             MyBatis 缓存体系                    │
 │                                               │
 │  ┌─────────────┐    ┌─────────────────────┐  │
 │  │  一级缓存     │    │    二级缓存           │  │
 │  │  (SqlSession)│    │  (Mapper namespace)  │  │
 │  │              │    │                      │  │
 │  │  默认开启     │    │  需手动开启           │  │
 │  │  Session级   │    │  跨SqlSession共享     │  │
│  │  HashMap     │    │  可集成第三方          │  │
│  └──────┬──────┘    └──────────┬───────────┘  │
│         │                      │              │
│         └──────────┬───────────┘              │
│                    ▼                          │
│              ┌───────────┐                    │
│              │   数据库    │                   │
│              └───────────┘                    │
└──────────────────────────────────────────────┘
查询顺序:二级缓存 → 一级缓存 → 数据库

一级缓存

一级缓存是 SqlSession 级别的缓存,默认开启,不可关闭。

工作原理

同一次 SqlSession 中:
. 第一次查询 emp_id=1 → 查数据库 → 存入一级缓存
. 第二次查询 emp_id=1 → 直接从一级缓存获取
. 执行 insert/update/delete → 清空一级缓存
. session.commit() 或 session.close() → 一级缓存消失

一级缓存失效条件

条件说明
不同的 SqlSession不同 session 不共享缓存
执行了 insert/update/delete清空当前 session 的所有缓存
执行了 session.clearCache()手动清空
执行了 session.commit()清空缓存
执行了 flushCache=“true” 的查询该查询清空缓存

一级缓存范围

<!-- 默认:SESSION(SqlSession 范围) -->
<setting name="localCacheScope" value="SESSION"/>
<!-- STATEMENT:每条语句执行后清空(相当于关闭一级缓存) -->
<setting name="localCacheScope" value="STATEMENT"/>

二级缓存

二级缓存是 Mapper namespace 级别的缓存,跨 SqlSession 共享。

开启方式

<!-- 1. 全局开启(mybatis-config.xml) -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. Mapper XML 中声明 -->
<mapper namespace="com.example.mapper.EmpMapper">
    <cache/>  <!-- 简单声明 -->
</mapper>

cache 元素属性

<cache
    type="org.apache.ibatis.cache.impl.PerpetualCache"   <!-- 缓存实现类 -->
    eviction="LRU"       <!-- 淘汰策略 -->
    flushInterval="60000" <!-- 刷新间隔(毫秒) -->
    size="1024"          <!-- 最大对象数 -->
    readOnly="false"     <!-- 是否只读 -->
/>

eviction 淘汰策略说明

LRU(默认)最近最少使用

FIFO 先进先出

SOFT 软引用(内存不足时回收)

WEAK 弱引用(GC 时回收)

readOnly说明
true所有线程共享同一对象(性能高,不安全)
false(默认)每次返回缓存对象的副本(序列化,安全)

二级缓存使用条件

  1. 实体类必须实现 Serializable 接口(readOnly=false 时)
  2. 必须在 select 后 commit 或 close 才会写入二级缓存
  3. 执行 insert/update/delete 会自动清空该 namespace 的二级缓存
@Test
public void testSecondLevelCache() {
    // Session 1
    try (SqlSession session1 = factory.openSession()) {
        EmpMapper mapper1 = session1.getMapper(EmpMapper.class);
        Emp emp1 = mapper1.selectById(1);
         session1.commit();  // 提交后写入二级缓存
     }
    // Session 2
    try (SqlSession session2 = factory.openSession()) {
        EmpMapper mapper2 = session2.getMapper(EmpMapper.class);
        Emp emp2 = mapper2.selectById(1);  // 从二级缓存获取
    }
}

select 的 useCache 和 flushCache

 <!-- 该查询不使用二级缓存 -->
 <select id="selectCount" resultType="int" useCache="false">
     SELECT COUNT(*) FROM emp
 </select>
 <!-- 该查询执行后清空缓存 -->
 <select id="selectFreshData" resultType="Emp" flushCache="true">
     SELECT * FROM emp
 </select>
<!-- insert/update/delete 默认 flushCache="true" -->

cache-ref 引用其他命名空间缓存

<!-- 多个 Mapper 共享同一个缓存 -->
<mapper namespace="com.example.mapper.EmpMapper">
    <cache/>
</mapper>
<mapper namespace="com.example.mapper.EmpDeptMapper">
    <!-- 引用 EmpMapper 的缓存 -->
    <cache-ref namespace="com.example.mapper.EmpMapper"/>
</mapper>

集成第三方缓存(EhCache)

<!-- 依赖 -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>
 <!-- Mapper XML -->
 <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
 <!-- 或带配置 -->
 <cache type="org.mybatis.caches.ehcache.EhcacheCache">
     <property name="timeToIdleSeconds" value="3600"/>
     <property name="timeToLiveSeconds" value="7200"/>
     <property name="maxEntriesLocalHeap" value="1000"/>
     <property name="maxEntriesLocalDisk" value="10000000"/>
    <property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

一级缓存 vs 二级缓存

特性一级缓存二级缓存
范围SqlSessionMapper namespace
共享不共享跨 SqlSession 共享
默认开启关闭
可关闭可设置 STATEMENT可设置 cacheEnabled=false
失效增删改、commit、close增删改同 namespace
存储HashMap可自定义(EhCache/Redis)

主键回填与批量操作

主键回填

自增主键回填

<insert id="insert" parameterType="Emp"
        useGeneratedKeys="true"
        keyProperty="empId"
        keyColumn="emp_id">
    INSERT INTO emp (emp_name, email, salary)
    VALUES (#{empName}, #{email}, #{salary})
</insert>
Emp emp = new Emp();
emp.setEmpName("张三");
mapper.insert(emp);
System.out.println(emp.getEmpId());  // 自动回填的主键

非自增主键(selectKey)

 <!-- Oracle 序列 -->
 <insert id="insert" parameterType="Emp">
     <selectKey keyProperty="empId" resultType="int" order="BEFORE">
         SELECT SEQ_EMP.NEXTVAL FROM DUAL
     </selectKey>
     INSERT INTO emp (emp_id, emp_name) VALUES (#{empId}, #{empName})
 </insert>
 <!-- MySQL UUID -->
<insert id="insert" parameterType="Emp">
    <selectKey keyProperty="empId" resultType="string" order="BEFORE">
        SELECT REPLACE(UUID(), '-', '')
    </selectKey>
    INSERT INTO emp (emp_id, emp_name) VALUES (#{empId}, #{empName})
</insert>

批量插入

方式一:foreach 拼接 VALUES

<insert id="batchInsert" parameterType="list">
    INSERT INTO emp (emp_name, email, salary, dept_id, hire_date, status)
    VALUES
    <foreach collection="list" item="emp" separator=",">
        (#{emp.empName}, #{emp.email}, #{emp.salary}, #{emp.deptId}, #{emp.hireDate}, #
    {emp.status})
    </foreach>
</insert>
List<Emp> emps = new ArrayList<>();
emps.add(emp1);
emps.add(emp2);
mapper.batchInsert(emps);

方式二:Batch 执行器

 try (SqlSession session = factory.openSession(ExecutorType.BATCH, false)) {
     EmpMapper mapper = session.getMapper(EmpMapper.class);
     for (int i = 0; i < 10000; i++) {
         Emp emp = new Emp();
         emp.setEmpName("emp_" + i);
         mapper.insert(emp);
         if (i % 1000 == 0) {
            session.flushStatements();  // 每 1000 条执行一次
        }
    }
    session.commit();
}

方式三:ON DUPLICATE KEY UPDATE(MySQL 批量 upsert)

 <insert id="batchUpsert" parameterType="list">
     INSERT INTO emp (emp_id, emp_name, email, salary)
     VALUES
     <foreach collection="list" item="emp" separator=",">
         (#{emp.empId}, #{emp.empName}, #{emp.email}, #{emp.salary})
     </foreach>
     ON DUPLICATE KEY UPDATE
         emp_name = VALUES(emp_name),
         email = VALUES(email),
        salary = VALUES(salary)
</insert>

批量更新

方式一:CASE WHEN

 <update id="batchUpdate" parameterType="list">
     UPDATE emp SET
     salary =
     <foreach collection="list" item="emp" open="CASE emp_id" close="END">
         WHEN #{emp.empId} THEN #{emp.salary}
     </foreach>
     WHERE emp_id IN
     <foreach collection="list" item="emp" open="(" separator="," close=")">
         #{emp.empId}
    </foreach>
</update>

方式二:Batch 执行器

try (SqlSession session = factory.openSession(ExecutorType.BATCH)) {
    EmpMapper mapper = session.getMapper(EmpMapper.class);
    for (Emp emp : empList) {
        mapper.update(emp);
    }
    session.commit();
}

TypeHandler 类型处理器

什么是 TypeHandler

TypeHandler 用于 Java 类型和 JDBC 类型之间的转换:

Java 类型  ←→  JDBC 类型
String    ←→  VARCHAR
Integer   ←→  INT
Date      ←→  TIMESTAMP
List      ←→  VARCHAR (自定义)

内置 TypeHandler

MyBatis 内置了常用 Java 类型与 JDBC 类型之间的转换器。

TypeHandlerJava 类型JDBC 类型
BooleanTypeHandlerBooleanbooleanBOOLEAN
IntegerTypeHandlerIntegerintINTEGER
LongTypeHandlerLonglongBIGINT
StringTypeHandlerStringVARCHARCHARLONGVARCHAR
DateTypeHandlerDateTIMESTAMP
BigDecimalTypeHandlerBigDecimalDECIMALNUMERIC
ByteArrayTypeHandlerbyte[]BLOBLONGVARBINARY
EnumTypeHandlerEnum通常为 VARCHAR
EnumOrdinalTypeHandlerEnum 的序号INTEGER
LocalDateTypeHandlerLocalDateDATE
LocalDateTimeTypeHandlerLocalDateTimeTIMESTAMP

自定义 TypeHandler

场景:将 List 存为 JSON 字符串

package com.example.handler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
    // 将 Java 类型 → JDBC 类型(存入数据库)
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                    List<String> parameter, JdbcType jdbcType) throws SQLException {
        // List → "a,b,c"
        String value = String.join(",", parameter);
        ps.setString(i, value);
    }
    // 将 JDBC 类型 → Java 类型(从数据库读取)
    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return toList(value);
    }
    @Override
    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return toList(value);
    }
    @Override
    public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return toList(value);
    }
    private List<String> toList(String value) {
        if (value == null || value.isEmpty()) {
            return new ArrayList<>();
        }
        return Arrays.asList(value.split(","));
    }
}

注册 TypeHandler

<!-- mybatis-config.xml -->
<typeHandlers>
    <!-- 方式一:单个注册 -->
    <typeHandler handler="com.example.handler.StringListTypeHandler"/>
    <!-- 方式二:包扫描 -->
    <package name="com.example.handler"/>
</typeHandlers>

使用 TypeHandler

 <!-- 在映射中指定 typeHandler -->
 <resultMap id="empResultMap" type="Emp">
     <id property="empId" column="emp_id"/>
     <result property="empName" column="emp_name"/>
     <result property="tags" column="tags"
             typeHandler="com.example.handler.StringListTypeHandler"/>
 </resultMap>
 <!-- 在参数中指定 -->
<insert id="insert" parameterType="Emp">
    INSERT INTO emp (emp_name, tags)
    VALUES (#{empName}, #{tags, typeHandler=com.example.handler.StringListTypeHandler})
</insert>

枚举类型处理

 public enum EmpStatus {
     ACTIVE(1, "在职"),
     RESIGNED(0, "离职");
     private int code;
     private String desc;
     EmpStatus(int code, String desc) {
         this.code = code;
        this.desc = desc;
    }
    // getter...
}
<!-- 方式一:按名称存储(默认) -->
<!-- 存入 "ACTIVE",读取时 Enum.valueOf("ACTIVE") -->
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
<!-- 方式二:按序号存储 -->
<!-- 存入 0(ACTIVE 的序号) -->
<setting name="defaultEnumTypeHandler"
    value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

自定义枚举 TypeHandler(按 code 存储)

 @MappedTypes(EmpStatus.class)
 public class EmpStatusTypeHandler extends BaseTypeHandler<EmpStatus> {
     @Override
     public void setNonNullParameter(PreparedStatement ps, int i,
                                     EmpStatus parameter, JdbcType jdbcType) throws SQLException {
         ps.setInt(i, parameter.getCode());
     }
     @Override
    public EmpStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return EmpStatus.fromCode(code);
    }
    // ... 其他方法
}

ObjectFactory 对象工厂

什么是 ObjectFactory

ObjectFactory 负责创建结果对象的实例。默认实现是 DefaultObjectFactory。

自定义 ObjectFactory

 package com.example.factory;
 import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
 import java.util.List;
 import java.util.Properties;
 public class CustomObjectFactory extends DefaultObjectFactory {
     @Override
    public <T> T create(Class<T> type) {
        T object = super.create(type);
        // 可以在这里做初始化
        return object;
    }
    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object>
     constructorArgs) {
        T object = super.create(type, constructorArgTypes, constructorArgs);
        // 自定义初始化逻辑
        return object;
    }
    @Override
    public void setProperties(Properties properties) {
        super.setProperties(properties);
    }
    @Override
    public <T> boolean isCollection(Class<T> type) {
        return super.isCollection(type);
    }
}
<objectFactory type="com.example.factory.CustomObjectFactory">
    <property name="someProperty" value="100"/>
</objectFactory>

Plugin 插件机制(拦截器)

插件原理

MyBatis 允许通过插件(拦截器)拦截以下四大对象的方法调用:

对象拦截的方法说明
Executorupdate, query, commit, rollback, createStatement 等执行器
ParameterHandlersetParameters参数处理
ResultSetHandlerhandleResultSets结果集处理
StatementHandlerprepare, parameterize, batchSQL 语句处理
SqlSession.selectList()
    → Executor.query()
        → StatementHandler.prepare()      ← 可拦截(修改 SQL)
        → StatementHandler.parameterize()
            → ParameterHandler.setParameters()  ← 可拦截(修改参数)
        → StatementHandler.query()
            → ResultSetHandler.handleResultSets()  ← 可拦截(修改结果)

编写自定义插件

分页拦截器示例

package com.example.plugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
 import org.apache.ibatis.plugin.*;
 import org.apache.ibatis.session.ResultHandler;
 import org.apache.ibatis.session.RowBounds;
 import java.util.Properties;
@Intercepts(@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PagePlugin implements Interceptor {
    private Properties properties;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        // 执行原查询(获取总数)
        // ... 分页逻辑
        // 修改 RowBounds
        args[2] = new RowBounds(offset, limit);
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}
<!-- 注册插件 -->
<plugins>
    <plugin interceptor="com.example.plugin.PagePlugin">
        <property name="dialect" value="mysql"/>
    </plugin>
</plugins>

SQL 审计拦截器示例

 @Intercepts({
     @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
 })
 public class SqlAuditPlugin implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
         MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        // 获取 SQL ID
        String sqlId = ms.getId();
        // 获取 SQL 语句
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();
        // 记录审计日志
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("[SQL审计] " + sqlId + " | 耗时: " + elapsed + "ms");
        System.out.println("[SQL] " + sql);
        return result;
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {}
}

分页

RowBounds 分页(内存分页)

// 内存分页(不推荐大数据量)
int offset = 0;
int limit = 10;
RowBounds rowBounds = new RowBounds(offset, limit);
List<Emp> emps = session.selectList("com.example.mapper.EmpMapper.selectAll", null,
    rowBounds);

注意:RowBounds 是内存分页,先查出全部数据再取子集,不推荐使用。

SQL 分页(推荐)

1List<Emp> selectByPage(@Param(“offset”) int offset, @Param(“limit”) int limit);

<select id="selectByPage" resultType="Emp">
    SELECT * FROM emp
    ORDER BY emp_id
    LIMIT #{offset}, #{limit}
</select>

PageHelper 分页插件(最常用)

 <!-- 依赖 -->
 <dependency>
     <groupId>com.github.pagehelper</groupId>
     <artifactId>pagehelper</artifactId>
     <version>5.3.3</version>
 </dependency>
 <!-- 或 MyBatis 专用版 -->
 <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>
 <!-- mybatis-config.xml 配置 -->
 <plugins>
     <plugin interceptor="com.github.pagehelper.PageInterceptor">
         <!-- 数据库方言 -->
         <property name="helperDialect" value="mysql"/>
         <!-- 分页合理化:页码 < 1 查第一页,> 最大页查最后一页 -->
         <property name="reasonable" value="true"/>
         <!-- 支持通过 Mapper 接口参数传递分页参数 -->
         <property name="supportMethodsArguments" value="true"/>
        <!-- 总数统计 -->
        <property name="params" value="count=countSql"/>
    </plugin>
</plugins>

使用 PageHelper

 // 方式一:静态方法调用
 PageHelper.startPage(1, 10);  // 第 1 页,每页 10 条
 List<Emp> emps = mapper.selectAll();
 // PageHelper 会自动追加 LIMIT 0, 10
 // 获取分页信息
 PageInfo<Emp> pageInfo = new PageInfo<>(emps);
 System.out.println("总记录数: " + pageInfo.getTotal());
 System.out.println("总页数: " + pageInfo.getPages());
System.out.println("当前页: " + pageInfo.getPageNum());
System.out.println("每页大小: " + pageInfo.getPageSize());
System.out.println("是否有下一页: " + pageInfo.isHasNextPage());
// 方式二:通过参数传递
List<Emp> emps = mapper.selectByParams(1, 10);
// PageHelper 自动处理
// 方式三:Lambda 方式
PageInfo<Emp> pageInfo = PageHelper.startPage(1, 10)
    .doSelectPageInfo(() -> mapper.selectAll());

PageInfo 属性

 public class PageInfo<T> {
     private int pageNum;        // 当前页
     private int pageSize;       // 每页大小
     private long total;         // 总记录数
     private int pages;          // 总页数
     private List<T> list;       // 数据列表
     private int prePage;        // 上一页
     private int nextPage;       // 下一页
     private boolean isFirstPage;    // 是否首页
    private boolean isLastPage;     // 是否末页
    private boolean hasPreviousPage; // 是否有上一页
    private boolean hasNextPage;     // 是否有下一页
    private int navigateFirstPage;   // 导航首页
    private int navigateLastPage;    // 导航末页
    private int[] navigatepageNums;  // 导航页码数组
}

注解开发

基本注解

public interface EmpMapper {
    // 查询
    @Select("SELECT * FROM emp WHERE emp_id = #{empId}")
     Emp selectById(Integer empId);
     // 查询列表
     @Select("SELECT * FROM emp WHERE dept_id = #{deptId}")
     List<Emp> selectByDeptId(Integer deptId);
    // 插入(主键回填)
    @Insert("INSERT INTO emp (emp_name, email, salary) VALUES (#{empName}, #{email}, #
     {salary})")
    @Options(useGeneratedKeys = true, keyProperty = "empId")
    int insert(Emp emp);
    // 更新
    @Update("UPDATE emp SET emp_name = #{empName}, salary = #{salary} WHERE emp_id = #
     {empId}")
    int update(Emp emp);
    // 删除
    @Delete("DELETE FROM emp WHERE emp_id = #{empId}")
    int deleteById(Integer empId);
    // 查询数量
    @Select("SELECT COUNT(*) FROM emp")
    int selectCount();
}

Results 和 Result 注解

 @Results({
     @Result(property = "empId", column = "emp_id", id = true),
     @Result(property = "empName", column = "emp_name"),
     @Result(property = "hireDate", column = "hire_date"),
     @Result(property = "dept", column = "dept_id",
             one = @One(select = "com.example.mapper.DeptMapper.selectById",
                        fetchType = FetchType.LAZY))
 })
 @Select("SELECT * FROM emp WHERE emp_id = #{empId}")
Emp selectByIdWithDept(Integer empId);

一对多注解

@Results({
    @Result(property = "deptId", column = "dept_id", id = true),
    @Result(property = "deptName", column = "dept_name"),
    @Result(property = "emps", column = "dept_id",
            many = @Many(select = "com.example.mapper.EmpMapper.selectByDeptId",
                         fetchType = FetchType.LAZY))
})
@Select("SELECT * FROM dept WHERE dept_id = #{deptId}")
Dept selectDeptWithEmps(Integer deptId);

动态 SQL 注解(@SelectProvider 等)

 // Provider 方式
 public class EmpSqlProvider {
     public String selectByCondition(final Emp emp) {
         return new SQL() {{
             SELECT("*");
             FROM("emp");
             if (emp.getEmpName() != null) {
                 WHERE("emp_name LIKE CONCAT('%', #{empName}, '%')");
            }
            if (emp.getDeptId() != null) {
                WHERE("dept_id = #{deptId}");
            }
            if (emp.getStatus() != null) {
                WHERE("status = #{status}");
            }
            ORDER_BY("emp_id DESC");
        }}.toString();
    }
}
// 接口
public interface EmpMapper {
    @SelectProvider(type = EmpSqlProvider.class, method = "selectByCondition")
    List<Emp> selectByCondition(Emp emp);
    @InsertProvider(type = EmpSqlProvider.class, method = "insert")
    int insert(Emp emp);
    @UpdateProvider(type = EmpSqlProvider.class, method = "update")
    int update(Emp emp);
    @DeleteProvider(type = EmpSqlProvider.class, method = "delete")
    int delete(Integer empId);
}

注解与 XML 对比

特性XML 映射注解映射
SQL 复杂度适合复杂 SQL 和大型动态 SQL适合简单 SQL,复杂场景通常需要 Provider
动态 SQL功能完整、可读性较好能实现,但代码通常更分散
可读性SQL 与 Java 代码分离简单语句直观
维护性复杂查询更易集中维护简单 CRUD 维护成本低
推荐场景复杂查询、多人协作项目简单 CRUD、少量固定 SQL

Spring 集成

依赖

 <dependency>
     <groupId>org.mybatis</groupId>
     <artifactId>mybatis-spring</artifactId>
     <version>3.0.3</version>
 </dependency>
 <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-jdbc</artifactId>
    <version>6.1.5</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.5</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>

Spring 配置(XML 方式)

 <!-- 数据源 -->
 <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
     <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
     <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb"/>
     <property name="username" value="root"/>
     <property name="password" value="root123"/>
     <property name="maximumPoolSize" value="10"/>
     <property name="connectionTimeout" value="30000"/>
 </bean>
<!-- SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    <property name="typeAliasesPackage" value="com.example.entity"/>
</bean>
<!-- Mapper 扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.example.mapper"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 事务管理 -->
<bean id="transactionManager"
     class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

Spring 配置(Java 配置方式)

 @Configuration
 @MapperScan("com.example.mapper")
 @EnableTransactionManagement
 public class MyBatisConfig {
     @Bean
     public DataSource dataSource() {
         HikariConfig config = new HikariConfig();
         config.setDriverClassName("com.mysql.cj.jdbc.Driver");
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("root123");
        config.setMaximumPoolSize(10);
        return new HikariDataSource(config);
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
     {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));
        factory.setTypeAliasesPackage("com.example.entity");
        return factory.getObject();
    }
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Service 层使用

 @Service
 @Transactional
 public class EmpServiceImpl implements EmpService {
     @Autowired
     private EmpMapper empMapper;
     @Override
     public Emp getEmpById(Integer empId) {
        return empMapper.selectById(empId);
    }
    @Override
    public void transferSalary(Integer fromId, Integer toId, BigDecimal amount) {
        Emp from = empMapper.selectById(fromId);
        Emp to = empMapper.selectById(toId);
        from.setSalary(from.getSalary().subtract(amount));
        to.setSalary(to.getSalary().add(amount));
        empMapper.update(from);
        // 模拟异常
        // int i = 1 / 0;
        empMapper.update(to);  // 异常时自动回滚
    }
}

Spring Boot 集成

依赖

 <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>
</dependency>

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 30000
mybatis:
  # 配置文件位置
  config-location: classpath:mybatis-config.xml
  # Mapper XML 位置
  mapper-locations: classpath:mapper/*.xml
  # 实体类包
  type-aliases-package: com.example.entity
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    lazy-loading-enabled: true
    default-executor-type: reuse
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
# PageHelper
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true

启动类

@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

完整项目结构

src/main/
├── java/com/example/
│   ├── Application.java          ← 启动类
│   ├── config/
│   │   └── MyBatisConfig.java    ← 可选:自定义配置
 │   ├── controller/
 │   │   └── EmpController.java
 │   ├── service/
 │   │   ├── EmpService.java
│   │   └── impl/EmpServiceImpl.java
│   ├── mapper/
│   │   ├── EmpMapper.java        ← Mapper 接口
│   │   └── DeptMapper.java
│   └── entity/
│       ├── Emp.java
│       └── Dept.java
└── resources/
    ├── application.yml
    ├── mybatis-config.xml        ← 可选
    ├── mapper/
    │   ├── EmpMapper.xml         ← Mapper XML
    │   └── DeptMapper.xml
    └── logback.xml

MyBatis-Plus

MyBatis-Plus 简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

核心特性

 ┌─────────────────────────────────────────────┐
 │           MyBatis-Plus 核心特性               │
 ├──────────────┬─────────────────────────────┤
 │  无侵入       │ 只做增强不做改变              │
 │  损耗小       │ 启动即注入基本 CURD           │
 │  强大的 CRUD  │ 内置通用 Mapper/Service      │
 │  Lambda 表达式│ 编译期检查字段名              │
 │  主键自动生成  │ 支持多种主键策略              │
 │  代码生成器    │ 快速生成代码                 │
│  分页插件      │ 内置分页                    │
│  逻辑删除      │ 内置逻辑删除                 │
│  乐观锁        │ 内置乐观锁                   │
│  自动填充      │ 自动填充创建/更新时间         │
└──────────────┴─────────────────────────────┘

依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.5</version>
</dependency>

实体类注解

 @TableName("emp")   // 指定表名
 public class Emp {
     @TableId(type = IdType.AUTO)   // 主键策略
     private Integer empId;
     @TableField("emp_name")        // 指定列名(驼峰自动映射时可省略)
     private String empName;
    @TableField("email")
    private String email;
    @TableField(exist = false)     // 非数据库字段
    private String tempField;
    @TableField(fill = FieldFill.INSERT)  // 插入时自动填充
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)  // 插入和更新时自动填充
    private LocalDateTime updateTime;
    @TableLogic                    // 逻辑删除字段
    @TableField("status")
    private Integer deleted;
    @Version                       // 乐观锁版本号
    private Integer version;
}
IdType说明
AUTO使用数据库自增主键
NONE不指定主键策略,由全局配置或开发者决定
INPUT插入前由开发者手动设置主键
ASSIGN_ID使用默认标识符生成器分配 ID,通常为雪花算法生成的长整型值
ASSIGN_UUID分配不带连字符的 UUID 字符串

BaseMapper 通用 CRUD

public interface EmpMapper extends BaseMapper<Emp> {
    // 自动拥有以下方法,无需编写 XML
}
// 使用
EmpMapper mapper = ...;
 // 插入
 mapper.insert(emp);
// 根据 ID 删除
mapper.deleteById(1);
// 根据 ID 更新
mapper.updateById(emp);
// 根据 ID 查询
Emp emp = mapper.selectById(1);
// 查询所有
List<Emp> list = mapper.selectList(null);
// 条件查询
List<Emp> list = mapper.selectList(
    new QueryWrapper<Emp>()
        .eq("dept_id", 1)
        .ge("salary", 10000)
        .like("emp_name", "张")
        .orderByDesc("salary")
);
// Lambda 条件查询(推荐)
List<Emp> list = mapper.selectList(
    new LambdaQueryWrapper<Emp>()
        .eq(Emp::getDeptId, 1)
        .ge(Emp::getSalary, new BigDecimal("10000"))
        .like(Emp::getEmpName, "张")
        .orderByDesc(Emp::getSalary)
);
// 查询数量
Long count = mapper.selectCount(null);
// 分页查询
Page<Emp> page = mapper.selectPage(
    new Page<>(1, 10),  // 第1页,每页10条
    new LambdaQueryWrapper<Emp>().eq(Emp::getStatus, 1)
);
List<Emp> records = page.getRecords();
long total = page.getTotal();

IService 通用 Service

// Service 接口
public interface EmpService extends IService<Emp> {}
// Service 实现
@Service
 public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService
     {}
 // 使用
 @Autowired
private EmpService empService;
// 保存
empService.save(emp);
// 批量保存
empService.saveBatch(empList);
// 批量保存(每100条提交一次)
empService.saveBatch(empList, 100);
// 根据 ID 更新
empService.updateById(emp);
// 保存或更新
empService.saveOrUpdate(emp);
// 根据 ID 查询
Emp emp = empService.getById(1);
// 查询列表
List<Emp> list = empService.list();
// 条件查询
List<Emp> list = empService.list(
    new LambdaQueryWrapper<Emp>().eq(Emp::getDeptId, 1)
);
// 分页
Page<Emp> page = empService.page(
    new Page<>(1, 10),
    new LambdaQueryWrapper<Emp>().eq(Emp::getStatus, 1)
);
// 链式查询
List<Emp> list = empService.lambdaQuery()
    .eq(Emp::getDeptId, 1)
    .ge(Emp::getSalary, new BigDecimal("10000"))
    .list();
// 链式更新
empService.lambdaUpdate()
    .eq(Emp::getDeptId, 1)
    .set(Emp::getSalary, new BigDecimal("20000"))
    .update();
// 链式删除
empService.lambdaUpdate()
    .eq(Emp::getStatus, 0)
    .remove();

条件构造器常用方法

方法作用
eq(column, value)等于
ne(column, value)不等于
gt(column, value)大于
ge(column, value)大于等于
lt(column, value)小于
le(column, value)小于等于
like(column, value)模糊匹配,值两侧添加通配符
likeLeft(column, value)左模糊匹配
likeRight(column, value)右模糊匹配
notLike(column, value)非模糊匹配
in(column, list)集合包含
notIn(column, list)集合不包含
isNull(column)判断为空
isNotNull(column)判断非空
between(column, v1, v2)区间匹配
notBetween(column, v1, v2)非区间匹配
orderByDesc(column)降序排序
orderByAsc(column)升序排序
groupBy(column)分组
having(condition)添加 HAVING 条件
last(sql)在语句末尾拼接 SQL,必须确保内容可信
select(columns...)指定查询列

分页插件配置

 @Configuration
 public class MybatisPlusConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         // 分页插件
         interceptor.addInnerInterceptor(
            new PaginationInnerInterceptor(DbType.MYSQL)
        );
        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

逻辑删除配置

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted   # 逻辑删除字段
      logic-delete-value: 1         # 已删除值
      logic-not-delete-value: 0     # 未删除值

自动填充配置

 @Component
 public class MyMetaObjectHandler implements MetaObjectHandler {
     @Override
     public void insertFill(MetaObject metaObject) {
         this.strictInsertFill(metaObject, "createTime", LocalDateTime.class,
     LocalDateTime.now());
         this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class,
     LocalDateTime.now());
     }
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class,
     LocalDateTime.now());
    }
}

代码生成器

MyBatis Generator (MBG)

 <!-- pom.xml -->
 <build>
     <plugins>
         <plugin>
             <groupId>org.mybatis.generator</groupId>
             <artifactId>mybatis-generator-maven-plugin</artifactId>
             <version>3.0.4</version>
             <configuration>
     <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>com.mysql</groupId>
                    <artifactId>mysql-connector-j</artifactId>
                    <version>8.3.0</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>
 <!-- src/main/resources/generatorConfig.xml -->
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE generatorConfiguration PUBLIC
     "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
     "https://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
 <generatorConfiguration>
     <context id="default" targetRuntime="MyBatis3">
        <!-- 不生成注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!-- 数据库连接 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mydb"
                        userId="root"
                        password="root123"/>
        <!-- 实体类生成路径 -->
        <javaModelGenerator targetPackage="com.example.entity"
                            targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!-- Mapper XML 生成路径 -->
        <sqlMapGenerator targetPackage="mapper"
                         targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- Mapper 接口生成路径 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper"
                             targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!-- 要生成的表 -->
        <table tableName="emp" domainObjectName="Emp"
               enableCountByExample="false"
               enableUpdateByExample="false"
               enableDeleteByExample="false"
               enableSelectByExample="false"
               selectByExampleQueryId="false"/>
        <table tableName="dept" domainObjectName="Dept"/>
    </context>
</generatorConfiguration>

1mvn mybatis-generator:generate

MyBatis-Plus 代码生成器

 // 依赖
 // mybatis-plus-generator 3.5.5
 // velocity-engine-core 2.3
 public class CodeGenerator {
     public static void main(String[] args) {
         FastAutoGenerator.create(
                 "jdbc:mysql://localhost:3306/mydb",
                 "root",
                "root123"
        )
        .globalConfig(builder -> {
            builder.author("example")
                   .outputDir(System.getProperty("user.dir") + "/src/main/java")
                   .commentDate("yyyy-MM-dd");
        })
        .packageConfig(builder -> {
            builder.parent("com.example")
                   .entity("entity")
                   .mapper("mapper")
                   .service("service")
                   .serviceImpl("service.impl")
                   .controller("controller")
                   .pathInfo(Collections.singletonMap(
                       OutputFile.xml,
                       System.getProperty("user.dir") + "/src/main/resources/mapper"));
        })
        .strategyConfig(builder -> {
            builder.addInclude("emp", "dept")     // 表名
                   .addTablePrefix("t_", "sys_")  // 去除表前缀
                   .entityBuilder()
                       .enableLombok()
                       .enableTableFieldAnnotation()
                       .logicDeleteColumnName("deleted")
                       .versionColumnName("version")
                   .mapperBuilder()
                       .enableMapperAnnotation()
                   .serviceBuilder()
                       .formatServiceFileName("%sService")
                       .formatServiceImplFileName("%sServiceImpl");
        })
        .templateEngine(new VelocityTemplateEngine())
        .execute();
    }
}

性能优化

SQL 优化

  1. 使用 #{} 而非 ${}(防止 SQL 注入 + 利用预编译缓存)
  2. 避免 SELECT *,只查需要的列**
  3. 合理使用索引(参考 MySQL 索引指南)
  4. 避免 N+1 问题(使用嵌套结果代替嵌套查询,或使用延迟加载)
  5. 大批量操作使用 Batch 执行器

缓存优化

  1. 合理使用二级缓存(读多写少的场景)
  2. 缓存粒度控制(useCache / flushCache)
  3. 集成 Redis 等分布式缓存(替代本地缓存)

延迟加载

<!-- 开启延迟加载,减少不必要的关联查询 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>

连接池优化

<!-- 使用 HikariCP 高性能连接池 -->
<dataSource type="POOLED">
    <property name="poolMaximumActiveConnections" value="20"/>
    <property name="poolMaximumIdleConnections" value="10"/>
    <property name="poolMaximumCheckoutTime" value="20000"/>
    <property name="poolPingEnabled" value="true"/>
    <property name="poolPingQuery" value="SELECT 1"/>
</dataSource>

批量操作优化

 // 使用 Batch 执行器
 try (SqlSession session = factory.openSession(ExecutorType.BATCH)) {
     EmpMapper mapper = session.getMapper(EmpMapper.class);
     for (int i = 0; i < 10000; i++) {
         mapper.insert(emp);
         if (i % 1000 == 0) {
             session.flushStatements();  // 分批 flush
         }
     }
    session.commit();
}

fetchSize 优化

<!-- 大数据量查询时设置 fetchSize,减少网络交互 -->
<select id="selectLargeData" resultType="Emp" fetchSize="1000">
    SELECT * FROM emp
</select>

开启日志查看 SQL(开发环境)

<settings>
    <setting name="logImpl" value="SLF4J"/>
</settings>

性能优化清单

优化项说明优先级
SQL 优化合理使用索引、避免全表扫描⭐⭐⭐
避免嵌套查询 N+1使用 join 或延迟加载⭐⭐⭐
批量操作Batch 执行器⭐⭐⭐
连接池调优合理设置连接数⭐⭐
二级缓存读多写少场景⭐⭐
延迟加载减少不必要查询⭐⭐
fetchSize大数据量查询
映射优化只映射需要的字段

常见问题与排错

BindingException:找不到映射语句

症状:org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

原因:Mapper 接口方法与 XML 中的 statement id 不匹配

解决

  1. 检查 namespace 是否与接口全限定名一致
  2. 检查 id 是否与方法名一致
  3. 检查 XML 文件是否被正确加载(mapper-locations 配置)

参数绑定错误

症状:There is no getter for property named ‘xxx’

原因:MyBatis 对单参数的处理规则

解决

// 方式一:加 @Param
List<Emp> select(@Param("empName") String empName);
// 方式二:用对象包装
List<Emp> select(Emp query);
// 方式三:用 Map
List<Emp> select(Map<String, Object> params);

中文乱码

解决

  1. 数据库 URL 加 characterEncoding=UTF-8
  2. XML 文件编码设为 UTF-8
  3. 配置 <setting name=“logImpl” value=“SLF4J”/> 确保日志正确

一级缓存导致数据不一致

场景:同一次 SqlSession 中,其他事务修改了数据,当前 session 仍返回旧数据

解决

<!-- 关闭一级缓存 -->
<setting name="localCacheScope" value="STATEMENT"/>
<!-- 或在查询时刷新 -->
<select id="selectFresh" flushCache="true">...</select>

二级缓存序列化错误

症状:NotSerializableException

解决:实体类实现 Serializable 接口

嵌套查询 N+1 问题

解决

  1. 使用嵌套结果(join)代替嵌套查询
  2. 开启延迟加载
  3. 使用 MyBatis-Plus 的批量查询

${} 导致 SQL 注入

解决

  1. 尽量使用 #{}
  2. 必须用 ${} 时(表名/列名/排序),做输入校验

自动映射不生效

检查

  1. mapUnderscoreToCamelCase 是否设为 true
  2. autoMappingBehavior 是否为 PARTIAL 或 FULL
  3. ResultMap 的 autoMapping 是否为 true

动态 SQL 中的比较符号问题

解决

<!-- 方式一:转义 -->
<if test="salary &gt; 10000">
<!-- 方式二:CDATA -->
<if test="salary <![CDATA[ > ]]> 10000">

PageHelper 分页不生效

检查

  1. PageHelper.startPage() 必须在查询方法前一行调用
  2. 确保只对紧随其后的第一个查询生效
  3. 检查插件配置是否正确

实战案例集

案例一:部门员工三级关联查询

 <!-- 查询部门→员工→项目(三级嵌套) -->
 <resultMap id="deptEmpProjectResultMap" type="Dept">
     <id property="deptId" column="dept_id"/>
     <result property="deptName" column="dept_name"/>
     <collection property="emps" ofType="Emp">
         <id property="empId" column="emp_id"/>
         <result property="empName" column="emp_name"/>
         <result property="salary" column="salary"/>
        <collection property="projects" ofType="Project">
            <id property="projectId" column="project_id"/>
            <result property="projectName" column="project_name"/>
            <result property="budget" column="budget"/>
        </collection>
    </collection>
</resultMap>
<select id="selectDeptEmpProject" resultMap="deptEmpProjectResultMap">
    SELECT
        d.dept_id, d.dept_name,
        e.emp_id, e.emp_name, e.salary,
        p.project_id, p.project_name, p.budget
    FROM dept d
    LEFT JOIN emp e ON d.dept_id = e.dept_id
    LEFT JOIN emp_project ep ON e.emp_id = ep.emp_id
    LEFT JOIN project p ON ep.project_id = p.project_id
    WHERE d.dept_id = #{deptId}
</select>

案例二:通用条件分页查询

 // DTO
 @Data
 public class EmpQueryDTO {
     private String empName;
     private Integer deptId;
     private BigDecimal minSalary;
     private BigDecimal maxSalary;
     private Integer status;
     private Integer pageNum = 1;
    private Integer pageSize = 10;
}
 <select id="selectByPage" resultType="Emp">
     SELECT
     <include refid="empColumns"/>
     FROM emp
     <where>
         <if test="empName != null and empName != ''">
             <bind name="likeName" value="'%' + empName + '%'"/>
             AND emp_name LIKE #{likeName}
         </if>
        <if test="deptId != null">
            AND dept_id = #{deptId}
        </if>
        <if test="minSalary != null">
            AND salary &gt;= #{minSalary}
        </if>
        <if test="maxSalary != null">
            AND salary &lt;= #{maxSalary}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
    ORDER BY emp_id DESC
</select>
// Service
public PageInfo<Emp> getEmpPage(EmpQueryDTO dto) {
    PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
    List<Emp> list = empMapper.selectByPage(dto);
    return new PageInfo<>(list);
}

案例三:批量导入(Excel 数据导入)

 @Transactional
 public void batchImport(List<EmpDTO> dtoList) {
     List<Emp> empList = dtoList.stream().map(dto -> {
         Emp emp = new Emp();
         emp.setEmpName(dto.getName());
         emp.setEmail(dto.getEmail());
         emp.setSalary(dto.getSalary());
         emp.setDeptId(dto.getDeptId());
         emp.setHireDate(dto.getHireDate());
        emp.setStatus(1);
        return emp;
    }).collect(Collectors.toList());
    // 分批插入,每 500 条一批
    int batchSize = 500;
    for (int i = 0; i < empList.size(); i += batchSize) {
        int end = Math.min(i + batchSize, empList.size());
        List<Emp> batch = empList.subList(i, end);
        empMapper.batchInsert(batch);
    }
}

案例四:自定义 TypeHandler(JSON 存储)

// 实体类
@Data
public class Emp {
    private Integer empId;
    private String empName;
    // JSON 格式存储到数据库
    private Map<String, Object> extInfo;
}
 @MappedTypes(Map.class)
 @MappedJdbcTypes(JdbcType.VARCHAR)
 public class JsonTypeHandler extends BaseTypeHandler<Map<String, Object>> {
     private static final ObjectMapper mapper = new ObjectMapper();
     @Override
     public void setNonNullParameter(PreparedStatement ps, int i,
                                     Map<String, Object> parameter, JdbcType jdbcType)
     throws SQLException {
        try {
            ps.setString(i, mapper.writeValueAsString(parameter));
        } catch (JsonProcessingException e) {
            throw new SQLException("JSON 序列化失败", e);
        }
    }
    @Override
    public Map<String, Object> getNullableResult(ResultSet rs, String columnName)
     throws SQLException {
        return parse(rs.getString(columnName));
    }
    @Override
    public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return parse(rs.getString(columnIndex));
    }
    @Override
    public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex)
     throws SQLException {
        return parse(cs.getString(columnIndex));
    }
    private Map<String, Object> parse(String json) {
        if (json == null || json.isEmpty()) return null;
        try {
            return mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
        } catch (JsonProcessingException e) {
            return null;
        }
    }
}

案例五:SQL 审计拦截器

 @Intercepts({
     @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class,
     ResultHandler.class}),
     @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
 })
 public class SlowSqlInterceptor implements Interceptor {
     private static final long THRESHOLD = 1000;  // 1秒
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();
        long start = System.currentTimeMillis();
        Object result;
        try {
            result = invocation.proceed();
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            if (elapsed > THRESHOLD) {
                log.warn("[慢SQL] {}ms | {} | {}", elapsed, ms.getId(), sql);
            } else {
                log.debug("[SQL] {}ms | {} | {}", elapsed, ms.getId(), sql);
            }
        }
        return result;
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {}
}

案例六:MyBatis-Plus 完整 CRUD

 // 实体类
 @TableName("emp")
 @Data
 public class Emp {
     @TableId(type = IdType.AUTO)
     private Long empId;
     @TableField("emp_name")
     private String empName;
    private String email;
    private BigDecimal salary;
    private Integer deptId;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableLogic
    private Integer deleted;
}
// Mapper
public interface EmpMapper extends BaseMapper<Emp> {
    // 自定义查询
    @Select("SELECT e.* FROM emp e WHERE e.salary > #{salary}")
    List<Emp> selectHighSalary(@Param("salary") BigDecimal salary);
}
// Service
@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {
    public Page<Emp> getEmpPage(int pageNum, int pageSize, EmpQueryDTO query) {
        return this.page(
            new Page<>(pageNum, pageSize),
            new LambdaQueryWrapper<Emp>()
                .like(StrUtil.isNotBlank(query.getEmpName()), Emp::getEmpName,
     query.getEmpName())
                .eq(query.getDeptId() != null, Emp::getDeptId, query.getDeptId())
                .ge(query.getMinSalary() != null, Emp::getSalary, query.getMinSalary())
                .le(query.getMaxSalary() != null, Emp::getSalary, query.getMaxSalary())
                .eq(Emp::getDeleted, 0)
                .orderByDesc(Emp::getCreateTime)
        );
    }
    public List<Emp> batchUpsert(List<Emp> empList) {
        this.saveOrUpdateBatch(empList, 500);
        return empList;
    }
}
// Controller
@RestController
@RequestMapping("/emp")
public class EmpController {
    @Autowired
    private EmpService empService;
    @GetMapping("/page")
    public Result<Page<Emp>> page(@RequestParam(defaultValue = "1") int pageNum,
                                   @RequestParam(defaultValue = "10") int pageSize,
                                   EmpQueryDTO query) {
        return Result.ok(empService.getEmpPage(pageNum, pageSize, query));
    }
    @PostMapping
    public Result<Void> save(@RequestBody Emp emp) {
        empService.save(emp);
        return Result.ok();
    }
    @PutMapping
    public Result<Void> update(@RequestBody Emp emp) {
        empService.updateById(emp);
        return Result.ok();
    }
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        empService.removeById(id);  // 逻辑删除
        return Result.ok();
    }
}

速查手册

核心配置速查

配置项默认值说明
mapUnderscoreToCamelCasefalse下划线命名自动映射为驼峰命名
cacheEnabledtrue是否允许使用映射器二级缓存
lazyLoadingEnabledfalse是否启用延迟加载
defaultExecutorTypeSIMPLE默认执行器类型
localCacheScopeSESSION一级缓存作用域
jdbcTypeForNullOTHERnull 参数使用的 JDBC 类型
useGeneratedKeysfalse是否允许 JDBC 自动生成主键
autoMappingBehaviorPARTIAL自动映射级别

动态 SQL 速查

元素用途说明
<if>条件判断条件成立时拼接 SQL 片段
<where>生成 WHERE自动移除开头多余的 ANDOR
<set>生成 SET自动移除末尾多余的逗号
<choose>多分支选择<when><otherwise> 配合
<trim>自定义裁剪使用前缀、后缀和覆盖规则调整 SQL
<foreach>遍历集合常用于 IN 条件和批量写入
<bind>绑定表达式结果可预先构造供后续引用的变量
<sql>定义 SQL 片段通过 <include> 复用

关联映射速查

元素用途常用属性
<association>一对一关联propertyjavaTypecolumnselectfetchType
<collection>一对多关联propertyofTypecolumnselectfetchType
<discriminator>根据列值选择映射columnjavaTypecase

参数占位符速查

特性#{}${}
处理方式预编译参数占位字符串直接替换
SQL 注入风险较低较高
典型用途参数值经过白名单校验的表名、列名或排序片段

缓存速查

特性一级缓存二级缓存
作用域SqlSessionMapper 命名空间
默认状态开启需要在映射文件中配置
常见失效时机更新、提交、回滚、关闭会话或清空缓存同命名空间执行更新语句时通常会刷新

注解速查

注解说明
@Select声明查询语句
@Insert声明插入语句
@Update声明更新语句
@Delete声明删除语句
@Results@Result声明结果映射
@One@Many声明关联查询
@Options配置主键回填、缓存等选项
@Param为方法参数命名

常见错误速查

现象常见原因排查方向
BindingException映射语句未注册检查命名空间、语句 ID 和映射文件扫描路径
找不到属性的 getter参数名或表达式不匹配检查 @Param、实体属性与 OGNL 表达式
NotSerializableException二级缓存对象不可序列化让缓存对象实现 Serializable,或调整缓存实现
N+1 查询嵌套查询逐条执行评估联表查询、批量查询或延迟加载
中文乱码客户端、连接或数据库编码不一致统一字符集配置

附录:MyBatis 知识体系全景

 MyBatis 知识体系
 ├── 基础
 │   ├── 概述与架构
 │   ├── 快速入门
 │   ├── 核心配置文件
 │   └── SqlSession 与映射器

 ├── SQL 映射
 │   ├── Mapper XML(select/insert/update/delete)
│   ├── #{} vs ${}
│   ├── 参数传递(@Param/Map/对象/List)
│   ├── SQL 片段(<sql>/<include>)
│   └── 主键回填(useGeneratedKeys/selectKey)

├── 结果映射
│   ├── ResultMap(id/result/constructor)
│   ├── 自动映射(autoMappingBehavior)
│   ├── ResultMap 继承(extends)
│   └── 鉴别器(discriminator)

├── 动态 SQL
│   ├── if / where / set
│   ├── choose / when / otherwise
│   ├── trim / foreach / bind
│   └── SQL 片段复用

├── 关联查询
│   ├── 一对一(association)
│   ├── 一对多(collection)
│   ├── 多对多
│   ├── 嵌套结果 vs 嵌套查询
│   └── 延迟加载

├── 缓存
│   ├── 一级缓存(SqlSession)
│   ├── 二级缓存(namespace)
│   ├── 第三方缓存(EhCache/Redis)
│   └── cache-ref

├── 高级特性
│   ├── TypeHandler 类型处理器
│   ├── ObjectFactory 对象工厂
│   ├── Plugin 插件机制
│   ├── 分页(RowBounds/PageHelper)
│   └── 批量操作(foreach/Batch执行器)

├── 注解开发
│   ├── 基本 CRUD 注解
│   ├── @Results/@Result
│   ├── @One/@Many
│   └── @SelectProvider 等

├── 框架集成
│   ├── Spring 集成
│   ├── Spring Boot 集成
│   └── MyBatis-Plus

├── 工具
│   ├── 代码生成器(MBG/MP Generator)
│   └── 分页插件(PageHelper)

├── 性能优化
│   ├── SQL 优化
│   ├── 缓存策略
│   ├── 延迟加载
│   ├── 批量操作
│   ├── 连接池调优
│   └── fetchSize

└── 排错
    ├── BindingException
    ├── 参数绑定错误
    ├── 编码问题
    ├── 缓存问题
    └── N+1 问题

文档说明:本文档全面覆盖了 MyBatis 的所有知识点,从基础概念到高级实战,从理论到指令操作,无遗漏。适合初学者系统学习,也适合有经验的开发者作为参考手册。配合之前的 MySQL(JOIN/子查询/事务/索引)和 Maven 指南使用,可构建完整的 Java 后端知识体系。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka