A complete guide to MyBatis's complete knowledge system

Published 2026-07-30 19:31 Updated 2026-07-30 19:31 12638 words 64 min read ... Page views

This article comprehensively and systematically combs MyBatis 'complete knowledge base, from basic concepts to advanced practical combat, covering architectural principles, core configuration, dynamic SQL, associated queries, caching mechanisms, type processors, plug-in development, annotation use, Spring integration and MyBatis-Plus enhancements, and provides troubleshooting of common problems and performance optimization suggestions, suitable for beginners to learn and senior developers to refer to.

A complete guide to MyBatis’s complete knowledge system

This document comprehensively covers MyBatis ‘theoretical knowledge and command operations, from basic concepts to advanced actual combat, without omission.

MyBatis Overview

What is MyBatis

MyBatis is an SQL mapping and persistence layer framework. It allows developers to write their own SQL and is responsible for repetitive tasks such as parameter binding, result mapping, and transaction collaboration. MyBatis is often referred to as “semi-automated ORM”, but it does not automatically generate all SQL based on entity relationships like the fully automated ORM framework does.

MyBatis, formerly known as iBatis, was migrated from Apache to Google Code in 2010 and renamed MyBatis, and later migrated to GitHub.

MyBatis ‘positioning

FeaturesJDBCMyBatisHibernate (JPA)
SQLControlFully manualSemi-automatic (handwrittenSQL, automatic mapping)Fully automatic (automatically generatedSQL)
Learning costhigh (a lot of template codes)mediumhigh
FlexibilityHighestHighLower
has controllable performanceisisis difficult (needs to be adjusted)
database portabilitypoormediumhigh
is suitable for scenarios-Complex SQL, high-performance requirementsRapid development, simple CRUD

Core features of MyBatis

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

MyBatis Architecture

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

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

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

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

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

MyBatis version

MyBatis 3 is currently a commonly used main version series in courses and practical projects. 3.5.15 is used for the examples in this article, and the version number is only used to ensure that the examples are reproducible; the actual project should be based on the version determined in dependency management, and the release notes of the corresponding version should be consulted when upgrading.

quick start

sample database

This document uses the following example databases uniformly:

 -- 部门表
 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 Depends

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

entity class

 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 interface

 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>

tool class

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

test

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());
        }
    }
}

Core configuration file (mybatis-config.xml)

Complete configuration structure

 <?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>

Configure element order

The top-level elements of mybatis-config.xml must appear in the following order. Elements that are not used can be omitted, but the order cannot be exchanged at will.

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

Detailed explanation of common Settings

Setting ItemsOptional ValuesDefault ValuesDescription
cacheEnabledtrue, falsetrueWhether the mapper secondary cache is globally enabled
lazyLoadingEnabledtrue, falsefalseWhether delayed loading is enabled
aggressiveLazyLoadingtrue, falsefalseWhether all delay attributes of the trigger object are loaded by any method call
mapUnderscoreToCamelCasetrue, falsefalseWhether the underscore naming is automatically mapped to the hump naming
useGeneratedKeystrue, falsefalseDoes JDBC allow automatic generation of primary key
defaultExecutorTypeSIMPLE, REUSE, BATCHSIMPLEDefault actuator type
defaultStatementTimeoutpositive integerSQL execution timeout is not set, in seconds
localCacheScopeSESSION, STATEMENTSESSIONLevel 1 Cache Scope
jdbcTypeForNullJDBC typeOTHERSpecify the JDBC type
logImplLog Implementation ClassAutomatic DetectionSpecifies the log used by MyBatis to implement
autoMappingBehaviorNONE, PARTIAL, FULLPARTIALAutomatic mapping level

Three types of data sources

typeDescriptionApplication scenarios
UNPOOLEDOpen/close connection every requestSimple application
POOLEDconnection poolMost applications (recommended)
JNDIObtaining data sources from JNDIApplication servers (Tomcat, etc.)

Two transaction managers

typeDescriptionApplication scenarios
JDBCDirect use of JDBC commit/rollbackIndependent application
MANAGEDContainer Management Transactions (No Submission/Rollback)Application Server, Spring

type alias

Common aliases built into MyBatis:

AliasJavaType
_intint
intInteger
_longlong
longLong
_booleanboolean
booleanBoolean
stringString
integerInteger
dateDate
decimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList

SqlSession and Mapper Interface

SqlSession life cycle

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


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


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


Mapper 接口实例            →  SqlSession 级别

SqlSession core method

 // 查询
 <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();

Two ways of using

Method 1: Call directly through namespace

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

Recommended Method 2: Type safety, IDE prompt, compile period check.

Three actuators

ActuatorDescriptionApplication Scenarios
SimpleCreate a new Statement for each executiondefault
ReuseReuse StatementSame SQL multiple times
Batchbatch executionBatch INSERT/UPDATE
// 指定执行器
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

Affairs management

 // 手动提交(默认)
 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 file (Mapper XML)

complete structure

 <?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>

Detailed explanation of select element attributes

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

#The difference between {} and${}

Characteristics#{}${}
Processing MethodPrecompiled Parameters (PreparedStatement?)string splicing
SQL injectionPreventRisk
Performanceleverages pre-compiled cacheDifferent every time
usage scenariosparameter valuestable name /column name/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>
<!-- 必须对 ${} 的输入做校验 -->

parameter passing

single parameter

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

Multiple parameters (using @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>

Multiple parameters (using Map /Object)

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

Parameter is 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>

Details of insert elements

 <!-- 普通插入 -->
 <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 PropertiesDescription
keyPropertyJava attribute to be written to the primary key value
keyColumnPrimary key column name in the database.
resultTypeJava type of primary key value
orderBEFORE represents the query primary key before insertion, and AFTER represents the query primary key after insertion

SQL fragment

<!-- 定义 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>

Detailed explanation of ResultsMap

Why you need a ResultsMap

When database column names are inconsistent with Java attribute names, mapping needs to be carried out through ResultMap:

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

Basic structure of 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 map

 // 实体类有带参构造器
 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 automatic mapping

 <!-- 全局设置 -->
 <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 inheritance

 <!-- 基础 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

Decide to use a different ResultMap based on the value of a column:

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

dynamic SQL

Overview of dynamic SQL elements

ElementDescriptionis similar to
<if>Condition Judgmentif
<choose>/<when>/<otherwise>Multi-condition selectionswitch/case/default
<where>Smart WHERE clause-
<set>Smart SET clause-
<trim>Custom prefix/suffix clipping-
<foreach>traversal setfor
<bind>Binding variables-
<sql>/<include>SQL fragment reuse-

if element

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

Note: In XML,< 和 > needs to be escaped to&lt; and&gt;, or wrapped with<![CDATA[…]]>.

where element

<where> will automatically process:

  1. Remove the first AND or OR
  2. If there are no conditions, no WHERE is added
 <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 element

<set> will automatically process:

  1. Remove unnecessary commas at the end
  2. If there are no conditions, no SET will be added (an error will be reported)
 <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 element

<trim> is one of the basic capabilities of elements such as <where> and <set>. It can customize prefixes, suffixes, and content that needs to be removed.

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

<!-- 等价于 <set> 的常见写法 -->
<trim prefix="SET" suffixOverrides=",">
    ...
</trim>
PropertiesDescription
prefixAdd prefix
suffixAdd suffix
prefixOverridesRemove the matching string
suffixOverridesRemove the matching string

foreach element

 <!-- 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>
PropertiesDescription
collectionThe name of the set to traverse. A single List parameter is usually list, an array is usually array, or you can specify the name
itemThe variable name of the current element
indexKey for current index or Map
openStarting string for the entire paragraph
closeEnd string for the entire paragraph
separatorSeparators between adjacent elements

bind element

 <!-- 模糊查询拼接 -->
 <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>

Complete dynamic SQL example

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

relational query

One-to-one association

Method 1: Nested results (join query, once 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>

Method 2: Nested query (step-by-step query, N+1 questions)

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

Implementation process:

  1. Select * FROM emp WHERE emp_id = ?
  2. Remove dept_id and execute SELECT * FROM DEPT WHERE dept_id = ?

Method 3: Nested query passes multiple parameters

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

One-to-many association (collection)

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

Method 1: Nested results

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

Method 2: Nested query

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

many-to-many correlation

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>

Nested query vs nested result comparison

FeaturesNested Results (join)Nested Query (Step by Step**)**
SQL times1 times (multi-table JOIN**)**N+1 times
performancehigh (single query )low (N+1 problem, but can delay loading**)**
Delayed loadingDoes not supportSupports
SQL complexityhigh (multi-table JOIN**)**low (simple query**)**
data volumelarge (Cartesian product )step-by-step acquisition
recommendedless associated datamore associated data + delayed loading

Delayed loading configuration

<!-- 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" .../>

caching mechanism

cache system

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

a level one cache

Level 1 cache is a cache at the SqlSession level, which is turned on by default and cannot be turned off.

working principle

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

Level 1 cache invalidation conditions

ConditionsDescription
Different SqlSessionsDifferent sessions do not share cache
executes insert/update/deleteclears all caches in the current session
executed session.clearCache()manually cleared
executed session.commit()cleared cache
executes a query with flashCache =“true”The query empties the cache

Level 1 cache range

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

second level cache

The secondary cache is a cache at the Mapper namespace level and is shared across SqlSessions.

opening method

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

cache element attributes

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

eviction Elimination Strategy Description

LRU (default) least recently used

FIFO First in First Out

SOFT soft references (recycled when memory is low)

WEAK weak reference (recycled during GC)

readOnlyDescription
trueAll threads share the same object (high performance, unsafe)
false (default)Returns a copy of the cached object each time (serialized, safe)

L2 cache usage conditions

  1. Entity classes must implement Serializable interface (when readOnly=false)
  2. Commit or close must be written to the secondary cache after select
  3. Executing insert/update/delete will automatically clear the secondary cache of the 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 and flashCache

 <!-- 该查询不使用二级缓存 -->
 <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 references other namespace caches

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

Integrated third-party cache (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>

Level 1 cache vs Level 2 cache

FeaturesPrimary cacheSecondary cache
RangeSqlSessionMapper namespace
sharesdoes not shareshares
DefaultOnOff
can be turned offcan be set STATEMENTcan be set cacheEnabled= false
is invalidadditions, deletions, commits, closeadditions, deletions and modifications are the same as namespace
StorageHashMapCustomizable (EhCache/Redis)

Primary key backfilling and batch operation

Backfill of primary key

Self-added PK Backfill

<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());  // 自动回填的主键

Non-self-increasing primary key (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>

bulk inserts

Method 1: foreach splicing 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);

Method 2: Batch actuator

 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();
}

Method 3: ON DUPLICATE KEY UPDATE (MySQL batch 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>

batch update

Method 1: 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>

Method 2: Batch actuator

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

TypeHandler type processor

What is TypeHandler

TypeHandler is used for conversion between Java types and JDBC types:

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

Built-in TypeHandler

MyBatis has built-in converters between commonly used Java types and JDBC types.

TypeHandlerJava TypeJDBC Type
BooleanTypeHandlerBooleanbooleanBOOLEAN
IntegerTypeHandlerIntegerintINTEGER
LongTypeHandlerLonglongBIGINT
StringTypeHandlerStringVARCHARCHARLONGVARCHAR
DateTypeHandlerDateTIMESTAMP
BigDecimalTypeHandlerBigDecimalDECIMALNUMERIC
ByteArrayTypeHandlerbyte[]BLOBLONGVARBINARY
EnumTypeHandlerEnumUsually VARCHAR
EnumOrdinalTypeHandlerEnumINTEGER
LocalDateTypeHandlerLocalDateDATE
LocalDateTimeTypeHandlerLocalDateTimeTIMESTAMP

Custom TypeHandler

Scenario: Save the List as a JSON string

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(","));
    }
}

Register TypeHandler

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

Using 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>

Enumeration type handling

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

Custom enumeration TypeHandler (stored by 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 Object Factory

What is ObjectFactory

ObjectFactory is responsible for creating instances of the resulting object. The default implementation is DefaultObjectFactory.

Customize 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 plug-in mechanism (interceptor)

Plug-in principle

MyBatis allows plug-ins (interceptors) to intercept method calls on the following four major objects:

Objectinterception methoddescription
Executorupdate, query, commit, rollback, createStatement, etc.executors
ParameterHandlersetParametersParameter processing
ResultsSetHandlerhandleResultsSetResult Set Processing
StatementHandlerprepare, parameterize, batchSQL statement processing
SqlSession.selectList()
    → Executor.query()
        → StatementHandler.prepare()      ← 可拦截(修改 SQL)
        → StatementHandler.parameterize()
            → ParameterHandler.setParameters()  ← 可拦截(修改参数)
        → StatementHandler.query()
            → ResultSetHandler.handleResultSets()  ← 可拦截(修改结果)

Write custom plug-ins

Example paging interceptor

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 Audit Interceptor Example

 @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) {}
}

paging

RowBounds paging (memory paging)

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

Note: RowBounds is a memory page. First find all data and then retrieve a subset. It is not recommended.

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 paging plug-in (most commonly used)

 <!-- 依赖 -->
 <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>

Using 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 attribute

 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;  // 导航页码数组
}

annotation development

basic annotation

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 and Result annotations

 @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);

one-to-many annotation

@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);

Dynamic SQL annotations (@SelectProvider, etc.)

 // 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);
}

Comparison of annotations and XML

FeaturesXML MappingComment Mapping
SQL complexityis suitable for complex SQL and large dynamic SQLis suitable for simple SQL, complex scenarios usually require Provider
Dynamic SQLhas complete functions and good readability.can implement it, but the code is usually more dispersed.
ReadabilitySeparation of SQL and Java codeSimple statements intuitive
MaintainabilityComplex query makes it easier to maintain centralizedSimple CRUD Low maintenance cost
Recommended ScenariosComplex query, multi-person collaboration projectSimple CRUD, small amount of fixed SQL

Spring Integration

dependent

 <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 configuration (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 configuration (Java configuration method)

 @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 layer usage

 @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 integration

dependent

 <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

startup class

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

Complete project structure

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

Introduction to MyBatis-Plus

MyBatis-Plus (MP for short) is an enhancement tool for MyBatis. Based on MyBatis, it only enhances and does not change. It is created to simplify development and improve efficiency.

core features

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

dependent

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

Entity class annotation

 @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;
}
IdTypeDescription
AUTOUse the database self-added primary key
NONEdoes not specify a primary key policy, it is decided by the global configuration or developer
INPUTThe developer manually sets the primary key
ASSIGN_IDuses a default identifier generator to assign an ID, usually a long integer value generated by the snowflake algorithm
ASSIGN_UUIDAssign the UUID string without hyphen

BaseMapper Universal 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 General 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();

Common methods for condition builders

MethodAction
eq(column, value)equals
ne(column, value)is not equal to
gt(column, value)is greater than
ge(column, value)is greater than or equal to
lt(column, value)is less than
le(column, value)is less than or equal to
like(column, value)Fuzzy match, add wildcard
likeLeft(column, value)Left fuzzy match
likeRight(column, value)Right fuzzy match
notLike(column, value)Non-fuzzy matching
in(column, list)set contains
notIn(column, list)collection does not contain
isNull(column)judged empty
isNotNull(column)Judgment is not empty
between(column, v1, v2)Interval matching
notBetween(column, v1, v2)Non-interval matching
orderByDesc(column)Descending Sort
orderByAsc(column)Sort in ascending order
groupBy(column)Group
having(condition)Add HAVING Conditions
last(sql)When splicing SQL at the end of a statement, you must ensure that the content is trustworthy
select(columns...)Specify query column

paging plug-in configuration

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

Logical delete configuration

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

Automatic fill configuration

 @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());
    }
}

code generator

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 code generator

 // 依赖
 // 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();
    }
}

performance optimization

SQL Tuning

  1. Use#{}instead of ${}(prevent SQL injection+ utilize precompiled caching)
  2. **Avoid SELECT **and only check the required columns**3.Reasonable use of index(refer to MySQL Index Guide)
  3. Avoid N+1 issues(use nested results instead of nested queries, or use delayed loading)
  4. Use Batch actuators for large batch operations

cache optimization

  1. Reasonably use L2 cache(scenario where reading more and writing less)
  2. Cache granularity control(useCache /flashCache)
  3. Integrate distributed caching such as Redis(replaces local caching)

lazy loading

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

Connection pool optimization

<!-- 使用 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 operation optimization

 // 使用 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 optimization

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

Open the log to view SQL (development environment)

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

Performance optimization checklist

Optimization ItemsDescriptionPriority
SQL optimizesReasonable use of indexes and avoid full table scanning
Avoid nested queries N+1using join or delayed loading
Batch OperationBatch Actuator
Connection pool tuningReasonably set the number of connections
L2 cacheRead more and write less scenes
Delays loadingreduces unnecessary inquiries
fetchSizeLarge data query
Mapping Optimizationmaps only the required fields

Frequently Asked Questions and Troubleshooting

BindingException: Unable to find mapping statement

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

Reason: Mapper interface method does not match statement id in XML

Solution:

  1. Check whether the namespace is consistent with the fully qualified name of the interface
  2. Check whether the id is consistent with the method name
  3. Check that the XML file is loaded correctly (mapper-locations configuration)

Parameter binding error

Symptoms: There is no getter for property named ‘xxx’

Reason: MyBatis ‘handling rules for single parameters

Solution:

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

Chinese garbled code

Solution:

  1. Database URL Plus characterEncoding=UTF-8
  2. Set XML file encoding to UTF-8
  3. Configure<setting name=“logImpl” value=“SLF4J”/> Ensure that the logs are correct

Level 1 caching causes data inconsistency

Scenario: In the same SqlSession, other transactions modified data, but the current session still returns old data

Solution:

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

L2 cache serialization error

Symptom: NotSerializableException

Solution: Entity classes implement Serializable interfaces

Nested query N+1 questions

Solution:

  1. Use nested results (joins) instead of nested queries
  2. Enable delayed loading
  3. Batch query using MyBatis-Plus

${} caused SQL injection

Solution:

  1. Try to use#{}
  2. When${} must be used (table name/column name/sorting), perform input verification

Automatic mapping does not take effect

Inspection:

  1. Whether mapUnderscoreToCamelCase is set to true
  2. Whether autoMappingBehavior is PARTIAL or FULL
  3. Whether the autoMapping of ResultMap is true

Comparison symbol issues in dynamic SQL

Solution:

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

PageHelper paging does not take effect

Inspection:

  1. PageHelper.startPage() must be called on the line before the query method
  2. Ensure that only the first query that follows is valid
  3. Check whether the plug-in configuration is correct

Practical case collection

 <!-- 查询部门→员工→项目(三级嵌套) -->
 <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>

Case 2: General conditional paging query

 // 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);
}

Case 3: Batch import (Excel data import)

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

Case 4: Custom TypeHandler (JSON storage)

// 实体类
@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;
        }
    }
}

Case 5: SQL Audit Interceptor

 @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) {}
}

Case 6: MyBatis-Plus Complete 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();
    }
}

Quick check manual

Quick check of core configuration

Configuration ItemDefault ValueDescription
mapUnderscoreToCamelCasefalseUnderline naming is automatically mapped to hump naming
cacheEnabledtrueDoes the mapper L2 cache allow
lazyLoadingEnabledfalseWhether delayed loading is enabled
defaultExecutorTypeSIMPLEDefault actuator type
localCacheScopeSESSIONLevel 1 Cache Scope
jdbcTypeForNullOTHERnull JDBC type used for parameters
useGeneratedKeysfalseDoes JDBC allow automatic generation of primary key
autoMappingBehaviorPARTIALAutomatic mapping level

Dynamic SQL quick check

ElementPurposeDescription
<if>Condition judgmentJoining SQL fragments when conditions are established
<where>Generating WHEREAutomatically removes excess AND or OR
<set>Generating SETAutomatically removes unnecessary comma at the end
<choose>Multi-branch selection, <when>, <otherwise> in conjunction with
<trim>Custom clippingAdjusting SQL
<foreach>traversal setis commonly used for IN conditional and batch writing
<bind>Binding expression resultVariable
<sql>defines SQL fragmentreuses

Quick check of association mapping

ElementsPurposeCommon Attributes
<association>One-to-one correlationproperty, javaType, column, select, fetchType
<collection>One-to-many correlationproperty, ofType, column, select, fetchType
<discriminator>Select mapping based on column valuescolumn, javaType, case

Quick check of parameter placeholders

Characteristics#{}${}
processing methodpre-compilation parameter occupiesstring directly replaces
SQL injection risklowerhigher
Typical usesparameter valueswhitelist verified table names, column names or sorting fragments

Cache quick check

FeaturesPrimary CacheSecondary Cache
ScopeSqlSessionMapper Namespace
default stateto open, you need to configure
Common failure timingupdates, commits, rolls back, closes a session, or empties cache

Quick check of annotations

NotesNotes
@Selectdeclaration query statement
@Insertdeclaration insertion statement
@UpdateStatement update statement
@Deletedeclaration delete statement
@Results, @ResultDeclaration result mapping
@One, @ManyStatement Related Query
@OptionsConfigure options such as primary key backfilling, caching, etc.
@ParamNamed

Quick check of common mistakes

PhenomenonCommon CausesInvestigation Direction
BindingExceptionMapping statement is not registeredCheck namespace, statement ID and mapping file scan path
cannot find getter for attributeparameter names or expressions do not matchCheck @Param, entity attributes and OGNL expressions
NotSerializableExceptionSecondary cache objects are not serializableLet cache objects implement Serializable, or adjust cache to implement
N+1 querynested query executionevaluation joint table query, batch query or delayed loading
Chinese garbled codeinconsistent client, connection or database codingUnified Character Set Configuration

Appendix: Panorama of MyBatis Knowledge System

 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 问题

Document Description: This document comprehensively covers all knowledge points of MyBatis, from basic concepts to advanced actual combat, from theory to command operations, without omission. It is suitable for beginners to learn systematically, and it is also suitable for experienced developers as a reference manual. Used in conjunction with previous MySQL (JOIN/Subquery/Transaction/Index) and Maven guides, a complete Java backend knowledge body can be built.

If you enjoyed this, leave a comment~

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