跳转到内容

MyBatis

MyBatis 是一款优秀的半自动 ORM 持久层框架,前身是 Apache 的 iBatis。它封装了 JDBC 的繁琐操作,让开发者只需要关注 SQL 本身,通过 XML 或注解将 Java 对象与 SQL 语句映射,同时保留了对 SQL 的完全控制权。


一、MyBatis 核心架构与原理

1.1 整体架构

┌──────────────────────────────────────────────────────────────┐
│                        接口层                                 │
│         SqlSession (API 入口,增删改查操作)                     │
├──────────────────────────────────────────────────────────────┤
│                      数据处理层                                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │ Parameter    │  │ SQL 执行     │  │ Result          │   │
│  │ Handler      │  │ (Executor)   │  │ SetHandler      │   │
│  │ 参数映射      │  │              │  │ 结果映射         │   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
├──────────────────────────────────────────────────────────────┤
│                      框架支撑层                                │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐   │
│  │ SQL 解析 │ │ 缓存机制  │ │ 事务管理  │ │ 插件机制     │   │
│  │ (XML/注解)│ │ (L1/L2)  │ │          │ │ (Interceptor)│   │
│  └──────────┘ └──────────┘ └──────────┘ └──────────────┘   │
├──────────────────────────────────────────────────────────────┤
│                      基础支持层                                │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐   │
│  │ 数据源    │ │ 反射模块  │ │ 日志模块  │ │ 类型转换     │   │
│  │ (DS Pool) │ │          │ │          │ │              │   │
│  └──────────┘ └──────────┘ └──────────┘ └──────────────┘   │
└──────────────────────────────────────────────────────────────┘

1.2 核心组件

组件

作用

生命周期

SqlSessionFactory

创建 SqlSession 的工厂,全局唯一

应用级别(单例)

SqlSession

执行 SQL、管理事务的会话对象

每次请求/方法级别(用完关闭)

Executor

底层执行器,负责 SQL 执行和缓存管理

与 SqlSession 相同

StatementHandler

封装 JDBC Statement,处理参数和结果集

单次 SQL 调用

ParameterHandler

将 Java 参数映射到 JDBC 参数

单次 SQL 调用

ResultSetHandler

将 JDBC ResultSet 映射为 Java 对象

单次 SQL 调用

TypeHandler

负责 Java 类型与 JDBC 类型的互相转换

全局

MappedStatement

封装一条 SQL 映射的完整信息(XML 中一个 <select>/<insert> 等)

全局

Configuration

全局配置对象,封装所有解析后的配置

应用级别

1.3 执行流程(完整链路)

1. 获取 SqlSession
   SqlSession session = sqlSessionFactory.openSession();

2. 获取 Mapper 代理(JDK 动态代理)
   UserMapper mapper = session.getMapper(UserMapper.class);

3. 调用 Mapper 方法
   User user = mapper.selectById(1);

4. 内部执行链路:
   MapperProxy (动态代理)
       → SqlSession.selectOne(statementId, parameter)
       → Configuration → MappedStatement(找到 SQL 配置)
       → Executor(处理缓存 → 执行)
       → StatementHandler(构建 JDBC Statement)
       → ParameterHandler(设置参数)
       → JDBC Statement.execute()
       → ResultSetHandler(映射结果集 → Java 对象)
       → 返回结果

5. 关闭 Session
   session.close();

1.4 MyBatis 核心原理总结

原理

说明

XML/注解解析

启动时解析 XxxMapper.xml 或注解,生成 MappedStatement 存入 Configuration

动态代理

sqlSession.getMapper(XxxMapper.class) 返回 JDK 动态代理对象,拦截方法调用转为 SQL 执行

反射 + 类型转换

参数设置和结果映射大量使用反射(读取/设置字段值),TypeHandler 负责类型转换

工厂模式

SqlSessionFactory、DataSourceFactory 等

建造者模式

SqlSessionFactoryBuilder 构建 SqlSessionFactory


二、MyBatis 各种标签详解

2.1 全局配置标签(mybatis-config.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <!-- 1. properties:引入外部属性文件 -->
  <properties resource="db.properties">
    <property name="key" value="value"/>
  </properties>

  <!-- 2. settings:全局行为设置 -->
  <settings>
    <setting name="cacheEnabled" value="true"/>                <!-- 二级缓存开关 -->
    <setting name="lazyLoadingEnabled" value="true"/>           <!-- 延迟加载 -->
    <setting name="aggressiveLazyLoading" value="false"/>        <!-- 按需加载(3.4.1+) -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>     <!-- 驼峰映射 ⭐ -->
    <setting name="logImpl" value="STDOUT_LOGGING"/>            <!-- 日志实现 -->
    <setting name="useGeneratedKeys" value="true"/>             <!-- 自动获取自增主键 -->
    <setting name="defaultExecutorType" value="SIMPLE"/>         <!-- 执行器类型 -->
    <setting name="localCacheScope" value="SESSION"/>           <!-- 一级缓存作用域 -->
  </settings>

  <!-- 3. typeAliases:类型别名 -->
  <typeAliases>
    <package name="com.example.entity"/>  <!-- 包扫描,别名 = 类名首字母小写 -->
    <typeAlias type="com.example.User" alias="User"/>
  </typeAliases>

  <!-- 4. typeHandlers:类型处理器 -->
  <!-- 5. objectFactory:对象工厂 -->
  <!-- 6. plugins:插件 -->
  <!-- 7. environments:环境配置 -->
  <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>

  <!-- 8. mappers:映射器 -->
  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
    <package name="com.example.mapper"/>  <!-- 包扫描 -->
  </mappers>
</configuration>

2.2 SQL 映射文件标签(XxxMapper.xml)

SELECT 查询

<select id="selectById" parameterType="int" resultType="User">
    SELECT * FROM t_user WHERE id = #{id}
</select>

<select id="selectByCondition" parameterType="map" resultMap="userResultMap">
    SELECT * FROM t_user
    WHERE 1=1
    <if test="name != null and name != ''">
        AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="status != null">
        AND status = #{status}
    </if>
    ORDER BY create_time DESC
</select>

&lt;select&gt; 属性说明

属性

说明

id

唯一标识,对应 Mapper 接口方法名

parameterType

参数类型(可省略,框架自动推断)

resultType

返回结果类型(实体类全限定名或别名)

resultMap

结果映射引用(与 resultType 二选一)

useCache

是否使用二级缓存(默认 true)

timeout

查询超时秒数

fetchSize

批量获取数量

statementType

STATEMENT / PREPARED(默认,预编译)/ CALLABLE

flushCache

执行前是否清空一二级缓存

INSERT / UPDATE / DELETE

<!-- 插入(自动获取主键) -->
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO t_user(name, age, email) VALUES(#{name}, #{age}, #{email})
</insert>

<!-- 插入(selectKey 方式获取主键,适用于不支持自增的数据库) -->
<insert id="insert2" parameterType="User">
    <selectKey keyProperty="id" resultType="int" order="BEFORE">
        SELECT seq_user.nextval FROM dual   <!-- Oracle 序列 -->
    </selectKey>
    INSERT INTO t_user(id, name) VALUES(#{id}, #{name})
</insert>

<!-- 更新 -->
<update id="update" parameterType="User">
    UPDATE t_user SET name=#{name}, age=#{age} WHERE id=#{id}
</update>

<!-- 删除 -->
<delete id="deleteById" parameterType="int">
    DELETE FROM t_user WHERE id=#{id}
</delete>

2.3 动态 SQL 标签(核心!面试高频)

&lt;if&gt; — 条件判断

<select id="selectByQuery" resultType="User">
    SELECT * FROM t_user WHERE 1=1
    <if test="name != null and name != ''">
        AND name = #{name}
    </if>
    <if test="age != null">
        AND age >= #{age}
    </if>
</select>

&lt;choose&gt; / &lt;when&gt; / &lt;otherwise&gt; — 多选一(相当于 switch-case)

<select id="selectByAny" resultType="User">
    SELECT * FROM t_user WHERE 1=1
    <choose>
        <when test="name != null">
            AND name = #{name}
        </when>
        <when test="email != null">
            AND email = #{email}
        </when>
        <otherwise>
            AND status = 1
        </otherwise>
    </choose>
</select>

&lt;where&gt; — 自动处理 WHERE 和 AND 前缀(代替 WHERE 1=1

<select id="selectByQuery2" resultType="User">
    SELECT * FROM t_user
    <where>
        <if test="name != null">AND name = #{name}</if>
        <if test="age != null">AND age >= #{age}</if>
    </where>
</select>
<!-- 自动处理:
  - 没有条件时,不生成 WHERE
  - 有条件时,自动去掉第一个 AND / OR
-->

&lt;set&gt; — 自动处理 UPDATE SET 中的逗号

<update id="updateSelective">
    UPDATE t_user
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="age != null">age = #{age},</if>
        <if test="email != null">email = #{email},</if>
    </set>
    WHERE id = #{id}
</update>
<!-- 自动去掉最后一个多余的逗号 -->

&lt;trim&gt; — 万能裁剪(&lt;where&gt;&lt;set&gt; 的底层实现)

<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="name != null">AND name = #{name}</if>
    <if test="age != null">AND age = #{age}</if>
</trim>

<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
</trim>

&lt;trim&gt; 四属性

属性

说明

prefix

在前面拼接的字符串

suffix

在后面拼接的字符串

prefixOverrides

去掉最前面的指定字符串(| 分隔)

suffixOverrides

去掉最后面的指定字符串

&lt;foreach&gt; — 集合遍历

<!-- IN 查询 -->
<select id="selectByIds" resultType="User">
    SELECT * FROM t_user WHERE id IN
    <foreach collection="list" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

<!-- 批量插入 -->
<insert id="batchInsert">
    INSERT INTO t_user(name, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.age})
    </foreach>
</insert>

&lt;foreach&gt; 属性

属性

说明

collection

集合参数名:list(List 默认)、array(数组默认)、Map 的 key、@Param 别名

item

集合中每个元素的别名

index

索引名(遍历 List 时是下标,Map 时是 Key)

open

循环开始拼接的字符串

close

循环结束拼接的字符串

separator

元素间分隔符

&lt;sql&gt; / &lt;include&gt; — SQL 片段复用

<!-- 定义 SQL 片段 -->
<sql id="baseColumns">
    id, name, age, email, create_time, update_time
</sql>

<sql id="baseConditions">
    <where>
        <if test="status != null">AND status = #{status}</if>
        <if test="deleted != null">AND is_deleted = #{deleted}</if>
    </where>
</sql>

<!-- 引用 -->
<select id="selectAll" resultType="User">
    SELECT <include refid="baseColumns"/> FROM t_user
    <include refid="baseConditions"/>
    ORDER BY create_time DESC
</select>

<!-- 跨 XML 引用 -->
<select id="selectAll" resultType="User">
    SELECT <include refid="com.example.mapper.UserMapper.baseColumns"/>
    FROM t_user
</select>

&lt;resultMap&gt; — 高级结果映射

<!-- 基础映射 -->
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>                      <!-- 主键-->
    <result property="name" column="user_name"/>          <!-- 普通字段 -->
    <result property="createTime" column="create_time"/>  <!-- 驼峰 - 下划线映射 -->
</resultMap>

<!-- 一对一关联 (association) -->
<resultMap id="orderMap" type="Order">
    <id property="id" column="id"/>
    <result property="orderNo" column="order_no"/>
    <!-- 嵌套查询(分两次 SQL) -->
    <association property="user" column="user_id"
                 javaType="User" select="selectUserById"/>
    <!-- 嵌套结果(一条 SQL join) -->
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="name" column="user_name"/>
    </association>
</resultMap>

<!-- 一对多关联 (collection) -->
<resultMap id="userOrdersMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <!-- 一对多 -->
    <collection property="orders" ofType="Order"
                column="id" select="selectOrdersByUserId"/>
    <!-- 或嵌套结果 -->
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
    </collection>
</resultMap>

<!-- 鉴别器 (discriminator) — 根据某字段值映射不同子类 -->
<resultMap id="vehicleMap" type="Vehicle">
    <discriminator javaType="int" column="vehicle_type">
        <case value="1" resultType="Car"/>
        <case value="2" resultType="Truck"/>
    </discriminator>
</resultMap>

&lt;bind&gt; — 创建变量

<select id="selectByName" resultType="User">
    <bind name="pattern" value="'%' + name + '%'"/>
    SELECT * FROM t_user WHERE name LIKE #{pattern}
</select>
<!-- 解决不同数据库 CONCAT 语法差异 -->

2.4 注解标签

@Select("SELECT * FROM t_user WHERE id = #{id}")
User selectById(@Param("id") int id);

@Insert("INSERT INTO t_user(name, age) VALUES(#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

@Update("UPDATE t_user SET name=#{name} WHERE id=#{id}")
int update(User user);

@Delete("DELETE FROM t_user WHERE id=#{id}")
int deleteById(int id);

// 动态 SQL 注解(复杂 SQL 不推荐,建议用 XML)
@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")
List<User> selectByCondition(Map<String, Object> params);

2.5 标签速查总表

标签

位置

用途

频率

&lt;select&gt;

Mapper.xml

查询语句

⭐⭐⭐⭐⭐

&lt;insert&gt;

Mapper.xml

插入语句

⭐⭐⭐⭐⭐

&lt;update&gt;

Mapper.xml

更新语句

⭐⭐⭐⭐⭐

&lt;delete&gt;

Mapper.xml

删除语句

⭐⭐⭐⭐

&lt;sql&gt;

Mapper.xml

定义可复用的 SQL 片段

⭐⭐⭐

&lt;include&gt;

Mapper.xml

引用 SQL 片段

⭐⭐⭐

&lt;resultMap&gt;

Mapper.xml

自定义结果映射

⭐⭐⭐⭐⭐

&lt;id&gt;

&lt;resultMap&gt;

主键映射

⭐⭐⭐⭐

&lt;result&gt;

&lt;resultMap&gt;

普通字段映射

⭐⭐⭐⭐

&lt;association&gt;

&lt;resultMap&gt;

一对一关联映射

⭐⭐⭐

&lt;collection&gt;

&lt;resultMap&gt;

一对多关联映射

⭐⭐⭐

&lt;discriminator&gt;

&lt;resultMap&gt;

鉴别器(很少用)

&lt;if&gt;

动态 SQL

条件判断

⭐⭐⭐⭐⭐

&lt;where&gt;

动态 SQL

自动处理 WHERE

⭐⭐⭐⭐⭐

&lt;set&gt;

动态 SQL

自动处理 SET

⭐⭐⭐⭐

&lt;choose&gt;/&lt;when&gt;/&lt;otherwise&gt;

动态 SQL

多选一

⭐⭐⭐

&lt;foreach&gt;

动态 SQL

集合遍历(IN/批量)

⭐⭐⭐⭐⭐

&lt;trim&gt;

动态 SQL

万能裁剪

⭐⭐⭐

&lt;bind&gt;

动态 SQL

创建变量

&lt;selectKey&gt;

&lt;insert&gt;

获取主键

⭐⭐

&lt;settings&gt;

mybatis-config

全局行为配置

⭐⭐⭐⭐

&lt;typeAliases&gt;

mybatis-config

类型别名

⭐⭐⭐

&lt;plugins&gt;

mybatis-config

插件(分页拦截器)

⭐⭐⭐


三、SQL 注入与 MyBatis 防御

3.1 什么是 SQL 注入?

攻击者通过构造恶意输入,拼接并执行非预期的 SQL 语句。

// 用户输入:' OR '1'='1' --
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
// 拼接后:SELECT * FROM users WHERE name = '' OR '1'='1' --'
// 结果:返回全部用户!

3.2 MyBatis 中的 #{} vs ${}

这是面试最高频考点!

对比维度

#{}

${}

处理方式

预编译(PreparedStatement),? 占位符

直接拼接,字符串替换

防 SQL 注入

✅ 安全

❌ 不安全

类型处理

自动设置 JDBC 类型参数

原样输出,无类型处理

适用场景

WHERE 条件中的值

动态表名、动态列名、ORDER BY、GROUP BY、LIKE 拼接

语法示例

WHERE name = #{name}

SELECT * FROM ${tableName}

原理对比

-- #{} 生成的预编译 SQL
SELECT * FROM t_user WHERE name = ?   -- 参数 'Tom' 作为值安全传入

-- ${} 生成的 SQL(直接拼接!)
SELECT * FROM t_user WHERE name = Tom -- 如果 Tom 是 ' OR '1'='1' 就注入了

3.3 ${} 的安全使用场景

<!-- 1. 动态表名/列名(无法用 #{}) -->
<select id="selectByTable">
    SELECT * FROM ${tableName} WHERE id = #{id}   <!-- ⚠️ tableName 必须服务端白名单校验 -->
</select>

<!-- 2. ORDER BY 排序字段 -->
<select id="selectByOrder">
    SELECT * FROM t_user ORDER BY ${orderColumn} ${orderDirection}
    <!-- 解决方法:服务端对 orderColumn 做白名单校验 -->
</select>

<!-- 3. LIKE 模糊查询 -->
<!-- ❌ 不安全 -->
<select id="selectLike">SELECT * FROM t_user WHERE name LIKE '%${name}%'</select>
<!-- ✅ 安全——用 #{} + bind 或 CONCAT -->
<select id="selectLike">SELECT * FROM t_user WHERE name LIKE CONCAT('%', #{name}, '%')</select>

3.4 防御 SQL 注入的完整策略

  1. 默认使用 #{}:所有参数值绑定都用 #{}
  2. 必须用 ${} 时加白名单校验
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "age", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS = Set.of("ASC", "DESC");

public List<User> selectByOrder(String orderColumn, String orderDirection) {
    if (orderColumn == null || !ALLOWED_COLUMNS.contains(orderColumn)) {
        throw new IllegalArgumentException("非法列名: " + orderColumn);
    }
    if (orderDirection == null || !ALLOWED_DIRECTIONS.contains(orderDirection.toUpperCase())) {
        throw new IllegalArgumentException("非法排序: " + orderDirection);
    }
    return mapper.selectByOrder(orderColumn, orderDirection.toUpperCase());
}
  1. 不要拼接 SQL 字符串
  2. 使用 ORM 框架(MyBatis 本身提供基础防护)
  3. 最小权限原则:数据库账户只给必要权限

四、MyBatis 缓存机制

4.1 一级缓存(SqlSession 级别)

默认开启,无需配置。

特性

说明

作用域

同一个 SqlSession 内

生命周期

SqlSession 创建 → 关闭(或执行 insert/update/delete)

清空条件

执行增删改操作、手动 clearCache()flushCache=true 的查询

命中条件

相同的 statementId + 参数 + RowBounds + 分页

SqlSession session = factory.openSession();
User u1 = session.selectOne("selectById", 1);  // 查数据库
User u2 = session.selectOne("selectById", 1);  // 命中缓存,不查数据库
// u1 == u2 为 true(同一个对象,引用相同)
session.close();

4.2 二级缓存(Mapper 级别)

默认关闭,需手动开启。

<!-- 1. mybatis-config.xml -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

<!-- 2. XxxMapper.xml 添加 <cache/> -->
<mapper namespace="com.example.mapper.UserMapper">
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>
</mapper>

&lt;cache&gt; 属性

属性

说明

默认值

eviction

回收策略:LRU / FIFO / SOFT / WEAK

LRU

flushInterval

刷新间隔(毫秒)

不设置(仅增删改时清空)

size

缓存对象数量

1024

readOnly

只读(true 时返回同一对象快但不安全)

false

二级缓存注意事项

  • 查出的对象需序列化(实现 Serializable
  • 跨 SqlSession 共享
  • 不建议在频繁修改的表上开启
  • 生产环境通常用 Redis 等外部缓存替代 MyBatis 二级缓存

4.3 缓存执行顺序

查询请求
  → 二级缓存(Mapper 级别,跨 SqlSession)
    命中?→ 返回
    未命中?↓
  → 一级缓存(SqlSession 级别)
    命中?→ 返回
    未命中?↓
  → 查询数据库 → 存入一级缓存 → 一级缓存提交后会刷入二级缓存

五、MyBatis 面试常问问题

Q1:MyBatis 中 #{} 和 ${} 的区别?(最高频)

见上文第三章,核心:预编译 vs 直接拼接;防 SQL 注入的角度回答。

Q2:MyBatis 是如何实现 Mapper 接口与 SQL 绑定的?

答案:JDK 动态代理 + XML 解析。

// 1. 启动时:解析 XML/注解 → 生成 MappedStatement → 存入 Configuration
// MappedStatement 包含:id(namespace.statementId)、SQL、参数映射、结果映射

// 2. 运行时:sqlSession.getMapper(UserMapper.class)
// → Configuration.getMapper() → MapperRegistry.getMapper()
// → MapperProxyFactory.newInstance()  ← JDK 动态代理

// 3. 调用 mapper.selectById(1) 时:
// → MapperProxy.invoke() 拦截
// → 根据「接口名.方法名」拼接 statementId = "com.xxx.mapper.UserMapper.selectById"
// → 从 Configuration 中获取 MappedStatement
// → 调用 SqlSession.selectOne(statementId, args)
// → Executor → StatementHandler → JDBC

关键代码概念

// MapperProxy 是 JDK 的 InvocationHandler
public class MapperProxy<T> implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        // 如果方法是 Object 的方法(toString/hashCode),直接执行
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 获取 MapperMethod,调用 execute
        MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }
}

Q3:MyBatis 和 Hibernate 的区别?如何选型?

对比维度

MyBatis

Hibernate

类型

半自动 ORM

全自动 ORM

SQL 控制

手动编写 SQL,灵活

自动生成(HQL),封装度高

性能优化

直接写 SQL 优化

掌握成本较高

学习曲线

低(会 SQL 即可)

高(需理解 Session/Lazy/Cascade 等)

数据库移植

差(SQL 可能绑定特定数据库)

好(方言自动适配)

适用场景

复杂查询多、性能要求高、SQL 优化频繁

CRUD 简单、数据库迁移需求、快速原型

动态 SQL

强大(&lt;if&gt;&lt;foreach&gt; 等标签)

较弱(需 Criteria API)

Q4:MyBatis 延迟加载(懒加载)的原理?

<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <!-- 分两次查询:查询用户 → 需要时才查订单 -->
    <collection property="orders" ofType="Order"
                column="id" select="selectOrdersByUserId"
                fetchType="lazy"/>   <!-- lazy(懒加载) / eager(立即加载) -->
</resultMap>

原理CGLIB/Javassist 动态代理

1. 查询 User 时,只查 User 表,不查关联的 Orders
2. MyBatis 为 User 生成代理对象(CGLIB)
3. 当第一次调用 user.getOrders() 时:
   → 代理对象拦截方法调用
   → 执行 selectOrdersByUserId(user.id)
   → 将查询结果设置到 orders 属性
   → 后续调用直接返回缓存值

注意事项

  • 需引入 CGLIB 或 Javassist
  • 延迟加载期间 SqlSession 必须存活(Spring 中使用需配置事务保证)
  • 可能导致 N+1 问题,在循环中访问关联对象会产生大量 SQL

Q5:MyBatis 插件原理(分页插件怎么实现)?

答案:责任链模式 + JDK 动态代理 + 拦截器。

// 自定义拦截器示例
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare",
               args = {Connection.class, Integer.class})
})
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 前置处理
        System.out.println("SQL执行前...");
        Object result = invocation.proceed();  // 执行原方法
        // 后置处理
        System.out.println("SQL执行后...");
        return result;
    }
}

可拦截的四大对象

拦截对象

可拦截方法

Executor

update, query, flushStatements, commit, rollback, getTransaction, close, isClosed

ParameterHandler

getParameterObject, setParameters

ResultSetHandler

handleResultSets, handleOutputParameters

StatementHandler

prepare, parameterize, batch, update, query

PageHelper 分页插件原理

1. PageHelper.startPage(1, 10)
   → 将分页参数存入 ThreadLocal

2. 执行查询 SQL 时,Executor 的 query 方法被拦截
   → 读取 ThreadLocal 中的分页参数
   → 先执行 SELECT COUNT(*) 查总数
   → 修改原 SQL,追加 LIMIT 子句
   → 执行分页查询
   → 封装 PageInfo 返回

3. ThreadLocal 用完清除,避免内存泄漏

Q6:MyBatis 一级缓存和二级缓存的区别?

对比维度

一级缓存

二级缓存

作用域

SqlSession

Mapper (namespace)

默认开启

✅ 是

❌ 否

跨 SqlSession

不共享

共享

存储方式

HashMap

可自定义(默认 HashMap)

清空时机

SqlSession close / 增删改操作 / clearCache()

增删改 / flush-interval 到期

序列化

不需要

需要(实现 Serializable)

适用场景

同一会话内重复查询

不经常变的基础数据

生产建议

默认使用

谨慎使用(推荐用 Redis 替代)

Q7:MyBatis 批量插入怎么做?有哪些方式?

<!-- 方式 1:foreach 拼接 SQL(推荐,简单高效) -->
<insert id="batchInsert">
    INSERT INTO t_user(name, age) VALUES
    <foreach collection="list" item="u" separator=",">
        (#{u.name}, #{u.age})
    </foreach>
</insert>
<!-- 注意:SQL 有长度限制,单次不建议超过 5000 条 -->

<!-- 方式 2:SqlSession Batch 模式 -->
<insert id="insert">INSERT INTO t_user(name,age) VALUES(#{name},#{age})</insert>
// 方式 2 的调用方式(JDBC 批量)
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : list) {
    mapper.insert(user);  // 只提交到批量队列,不立即执行
}
session.commit();  // 一次性批量执行
session.close();
// 缺点:无法获取每条记录的自增主键

// 方式 3:ExecutorType.BATCH + flushStatements(推荐)
// 每 1000 条 flush 一次,兼顾内存和性能

Q8:MyBatis 中 DAO 和 Mapper 的区别?如何传多个参数?

DAO vs Mapper

DAO 模式

Mapper 代理模式

实现

手动写实现类,调用 SqlSession API

MyBatis 动态代理自动生成实现

开发量

多(接口 + 实现类)

少(只需接口 + XML/注解)

推荐

❌ 老式写法

✅ 现在主流

多参数传递方式

// 方式 1:@Param(推荐,明确语义)
User selectByAgeAndName(@Param("age") int age, @Param("name") String name);
// XML: WHERE age = #{age} AND name = #{name}

// 方式 2:Map(不推荐,key 是字符串无类型安全)
Map<String, Object> params = Map.of("age", 18, "name", "Tom");

// 方式 3:对象(多个筛选条件建议封装为 Query 对象)
UserQuery query = new UserQuery();
query.setAge(18); query.setName("Tom");

// 方式 4:arg0/arg1 或 param1/param2(默认索引名,不推荐)
// XML: WHERE age = #{arg0} AND name = #{arg1}
// XML: WHERE age = #{param1} AND name = #{param2}

Q9:当实体类字段名和数据库列名不一致时,如何处理?

<!-- 方法 1:SQL 别名 -->
<select id="selectAll" resultType="User">
    SELECT id, user_name AS userName, create_time AS createTime FROM t_user
</select>

<!-- 方法 2:开启驼峰自动映射(最简单) -->
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

<!-- 方法 3:resultMap 显式映射(最灵活) -->
<resultMap id="userMap" type="User">
    <result property="userName" column="user_name"/>
    <result property="createTime" column="create_time"/>
</resultMap>

Q10:MyBatis-Plus 和 MyBatis 的关系?

对比

MyBatis

MyBatis-Plus

定位

基础持久层框架

MyBatis 的增强工具

CRUD

手写 SQL

内置通用 Mapper(BaseMapper)

分页

手动实现/PageHelper

内置分页插件

代码生成器

有(AutoGenerator)

乐观锁

手动实现

注解 + 拦截器自动处理

逻辑删除

手动实现

@TableLogic 注解自动

主键策略

手动处理

内置雪花算法等多种策略

兼容性

完全兼容 MyBatis,无侵入


附录:快速启动配置示例

Spring Boot 集成(application.yml)

# 数据源配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis 配置
mybatis:
  mapper-locations: classpath:mapper/*.xml       # Mapper XML 位置
  type-aliases-package: com.example.entity        # 实体类包(别名)
  configuration:
    map-underscore-to-camel-case: true            # 驼峰映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 日志
    cache-enabled: true                           # 二级缓存总开关

Maven 依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>