Mybatis

(一)使用入门

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.16</version>
</dependency>

在项目根目录下新建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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。

SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

我们可以从 SqlSessionFactory 中获得 SqlSession 的实例。SqlSession 提供了数据库执行 SQL 命令所需的所有方法。

可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。(旧方法)例如:

<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>
try (SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

新方法,更简洁、安全的方式,使用和指定语句的参数和返回值相匹配的接口,比如 BlogMapper.class:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}

对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

(二)作用域和生命周期

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

映射器实例

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

(三)XML 映射器

SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

1.select

基础用法(最简示例)

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>
  • id:命名空间内唯一标识,用于调用该查询语句;
  • parameterType:传入参数的类型(可选,MyBatis 可自动推断);
  • resultType:返回结果的类型(集合需指定元素类型,而非集合本身);
  • #{id}:预处理参数占位符,MyBatis 会自动转换为 JDBC 的 ?,避免 SQL 注入,且自动完成参数赋值。
类别属性名核心作用默认值
基础标识id命名空间内唯一标识,用于引用语句无(必填)
参数相关parameterType传入参数的类全限定名 / 别名(可选)unset(自动推断)
parameterMap引用外部参数映射(已废弃,不推荐使用)
结果映射resultType返回结果的类全限定名 / 别名(集合填元素类型),与 resultMap 二选一
resultMap引用外部结果映射(处理复杂映射场景),与 resultType 二选一
缓存控制flushCache执行语句时是否清空本地 / 二级缓存false
useCache是否将结果存入二级缓存true(select )
执行控制timeout数据库返回结果的超时时间(秒)unset(依赖驱动)
fetchSize建议驱动批量返回的行数unset(依赖驱动)
statementType指定使用的 JDBC 语句类型(STATEMENT/PREPARED/CALLABLE)PREPARED
结果集控制resultSetType结果集类型(FORWARD_ONLY/SCROLL_SENSITIVE 等)unset(依赖驱动)
多库适配databaseId适配不同数据库厂商,仅加载匹配当前 databaseId 的语句
特殊场景resultOrdered嵌套结果查询时,按顺序映射减少内存消耗(仅嵌套查询)false
resultSets多结果集场景,指定每个结果集名称(逗号分隔)
affectData增删改语句返回数据时,确保事务控制正确(3.5.12+)false

2.ResultMap 复杂结果映射

  • ResultMap 是 MyBatis 中最核心、最强大的元素
  • 作用:

    1. 将数据库查询结果 ResultSet → Java 对象(POJO/JavaBean)
    2. 解决:列名与属性名不一致
    3. 处理:一对一、一对多、多对多 关联关系
    4. 处理:继承、多态、鉴别器
    5. 省去 90% 以上 JDBC 手动封装代码

两种简单映射(不用写resultMap)

映射到 Map
<select id="selectUser" resultType="map">
    select id, username from user where id = #{id}
</select>
  • 结果:Map<String, Object>,列名 = key,值 = value
  • 缺点:无类型,可读性差
自动映射到 JavaBean
<select id="selectUser" resultType="User">
    select id, username, password from user
</select>
  • 规则:

    • 数据库列名 和 Java 属性名 完全相同 → 自动映射
    • 大小写不敏感(user_nameuserName 也能自动匹配)

列名不一致怎么办?

  1. SQL 里用 别名 as(最简单)
<select id="selectUser" resultType="User">
    select
        user_id as id,
        user_name as username
    from user
</select>
  1. 使用 显式 resultMap(更规范、复用性强)

ResultMap 基础语法

<resultMap id="userMap" type="User">
    <!-- 主键:必须用 id,提高缓存/嵌套性能 -->
    <id     property="id"       column="user_id" />
    <!-- 普通字段 -->
    <result property="username" column="user_name" />
    <result property="password" column="hashed_password" />
</resultMap>

<select id="selectUser" resultMap="userMap">
    select user_id, user_name, hashed_password
    from user where id = #{id}
</select>

标签说明

  • <id>:主键映射,必须写,用于区分对象、提高性能
  • <result>:普通属性映射
  • property:Java 类的 属性名
  • column:数据库 列名 / 别名
  • type:要映射的 Java 类型(全类名或别名)

高级映射:一对一 association

场景:一个博客 对应 一个作者(一对一)

方式 1:嵌套查询(简单,但有 N+1 问题)
<resultMap id="blogMap" type="Blog">
    <id property="id" column="id"/>
    <result property="title" column="title"/>
    <!-- 一对一:查询另一个方法 -->
    <association
        property="author"
        column="author_id"
        javaType="Author"
        select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogMap">
    select * from blog where id = #{id}
</select>

<select id="selectAuthor" resultType="Author">
    select * from author where id = #{author_id}
</select>
  • 优点:简单、清晰
  • 缺点:N+1 查询问题(查 1 篇博客 → 1 条 SQL;查 N 篇 → N+1 条)
方式 2:嵌套结果(联表查询,推荐,性能高)
<resultMap id="blogMap" type="Blog">
    <id property="id" column="blog_id"/>
    <result property="title" column="blog_title"/>

    <association property="author" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="username" column="author_username"/>
    </association>
</resultMap>

<select id="selectBlog" resultMap="blogMap">
    select
        b.id as blog_id,
        b.title as blog_title,
        a.id as author_id,
        a.username as author_username
    from blog b
    left join author a on b.author_id = a.id
    where b.id = #{id}
</select>
  • 优点:只执行 1 条 SQL
  • 注意:必须给列起别名,避免重复

高级映射:一对多 collection

场景:一个博客 对应 多篇文章(一对多)

public class Blog {
    private Integer id;
    private String title;
    private List<Post> posts; // 一对多
}

写法:嵌套结果(推荐)

<resultMap id="blogMap" type="Blog">
    <id property="id" column="blog_id"/>
    <result property="title" column="blog_title"/>

    <!-- 一对多:ofType 指定集合泛型 -->
    <collection property="posts" ofType="Post">
        <id property="id" column="post_id"/>
        <result property="title" column="post_title"/>
    </collection>
</resultMap>

<select id="selectBlog" resultMap="blogMap">
    select
        b.id as blog_id,
        b.title as blog_title,
        p.id as post_id,
        p.title as post_title
    from blog b
    left join post p on b.id = p.blog_id
    where b.id = #{id}
</select>

collection 关键属性

  • ofType="Post"集合中元素的类型(必须写)
  • javaType="ArrayList":集合本身类型(可省略)

鉴别器 discriminator(多态、switch)

  • 作用:根据某一列的值,动态选择不同的映射
  • 类似 Java switch
<resultMap id="vehicleMap" type="Vehicle">
    <id property="id" column="id"/>
    <result property="name" column="name"/>

    <discriminator javaType="int" column="vehicle_type">
        <case value="1" resultType="Car">
            <result property="doorCount" column="door_count"/>
        </case>
        <case value="2" resultType="Truck">
            <result property="load" column="load"/>
        </case>
    </discriminator>
</resultMap>
  • column="vehicle_type":判断依据列
  • <case value="1">:值为 1 时,映射成 Car

构造方法映射 constructor

  • 用于:不可变类、没有 setter 方法
<resultMap id="userMap" type="User">
    <constructor>
        <idArg column="user_id" javaType="int" name="id"/>
        <arg column="user_name" javaType="String" name="username"/>
    </constructor>
</resultMap>
  • idArg:构造方法中的主键参数
  • arg:普通参数
  • name:指定构造参数名(MyBatis 3.4.3+ 支持)

最佳实践

  1. 简单查询:resultType + 别名
  2. 字段不匹配:resultMap
  3. 关联查询:优先联表 + 嵌套结果
  4. 嵌套映射 必须写 id
  5. 复杂 resultMap 从简单到复杂,逐步迭代 + 单元测试

3.insert, update 和 delete

基础语法示例

<!-- 插入 -->
<insert id="insertAuthor">
  insert into Author (id, username, password, email)
  values (#{id}, #{username}, #{password}, #{email})
</insert>

<!-- 更新 -->
<update id="updateAuthor">
  update Author set username = #{username}, password = #{password}
  where id = #{id}
</update>

<!-- 删除 -->
<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>
属性名取值 / 默认值核心作用
id字符串(唯一)命名空间内唯一标识,与接口方法名对应
parameterType全类名 / 别名(默认 unset)指定传入参数类型(MyBatis 可自动推断,通常省略)
flushCachetrue(默认,增删改)执行语句时清空一级缓存 + 二级缓存,保证数据一致性
timeoutunset(默认)数据库超时时间(秒),超时抛出异常(依赖驱动)
statementTypePREPARED(默认)执行类型:STATEMENT/ PREPARED/ CALLABLE(对应 JDBC 的 3 种 Statement)
databaseIdunset(默认)多数据库适配,仅匹配当前数据库标识的语句会被加载

insert 特性-主键生成

自动生成主键(MySQL/SQL Server 等支持自增的数据库)

核心配置

  • useGeneratedKeys="true":启用 JDBC 的 getGeneratedKeys() 获取自增主键
  • keyProperty="id":将生成的主键赋值到参数对象的 id 属性(支持多列,逗号分隔)
  • keyColumn="id":主键列名(仅主键列非表第一列时需指定,如 PostgreSQL)

示例:单条插入(自增主键)

<!-- 无需手动传入 id,数据库自增后回填到 Author 对象的 id 属性 -->
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username, password, email)
  values (#{username}, #{password}, #{email})
</insert>
// 接口方法
int insertAuthor(Author author);

// 调用后获取主键
Author author = new Author("admin", "123456", "[email protected]");
mapper.insertAuthor(author);
System.out.println(author.getId()); // 输出数据库生成的自增 ID

示例:批量插入(自增主键)

<!-- 传入 List<Author>,批量插入并回填每个对象的 id -->
<insert id="batchInsertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username, password, email) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email})
  </foreach>
</insert>
// 接口方法
int batchInsertAuthor(List<Author> authorList);
手动生成主键(Oracle 等不支持自增的数据库)

核心标签:<selectKey>

属性名取值 / 作用
keyProperty主键赋值的目标属性(如 id)
resultType主键类型(int/String 等)
orderBEFORE(先生成主键再插入)/ AFTER(先插入再取主键,Oracle 常用)
statementTypePREPARED(默认)/ STATEMENT / CALLABLE

示例:Oracle 序列生成主键(BEFORE)

<insert id="insertAuthor">
  <!-- 先执行序列查询,生成主键并赋值到 author.id -->
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select SEQ_AUTHOR_ID.NEXTVAL from DUAL
  </selectKey>
  insert into Author (id, username, password, email)
  values (#{id}, #{username}, #{password}, #{email})
</insert>

示例:插入后获取主键(AFTER,适配特殊数据库)

<insert id="insertAuthor">
  insert into Author (username, password, email)
  values (#{username}, #{password}, #{email})
  <!-- 先插入,再查询主键赋值 -->
  <selectKey keyProperty="id" resultType="int" order="AFTER">
    select LAST_INSERT_ID() from DUAL
  </selectKey>
</insert>
特殊场景:插入返回结果集(PostgreSQL/MariaDB/SQL Server)

部分数据库支持 RETURNING/OUTPUT 子句,可直接返回插入 / 更新 / 删除后的完整数据,需用 <select> 标签:

<select id="insertAndGetAuthor" resultType="Author" flushCache="true" affectData="true">
  insert into Author (username, password, email)
  values (#{username}, #{password}, #{email})
  returning id, username, password, email <!-- PostgreSQL 语法 -->
</select>

<!-- 接口方法:直接返回插入后的 Author 对象 -->
Author insertAndGetAuthor(Author author);

update 特性-if 动态条件

<!-- 更新:支持动态条件/字段(配合 <if> 标签) -->
<update id="updateAuthorSelective">
  update Author
  <set>
    <if test="username != null">username = #{username},</if>
    <if test="password != null">password = #{password},</if>
    <if test="email != null">email = #{email}</if>
  </set>
  where id = #{id}
</update>

delete 特性-foreach 批量删除

<!-- 删除:批量删除(配合 foreach) -->
<delete id="batchDeleteAuthor">
  delete from Author where id in
  <foreach item="id" collection="list" open="(" separator="," close=")">
    #{id}
  </foreach>
</delete>

最佳实践

  1. 主键生成
  • MySQL/SQL Server:优先用 useGeneratedKeys + keyProperty(简洁高效);
  • Oracle:用 <selectKey + 序列 + order="BEFORE"
  • 分布式场景:避免数据库自增主键,改用雪花算法 / UUID(提前生成主键,无需依赖数据库)。
  1. 批量操作
  • 插入 / 删除:用 foreach 拼接 SQL(控制批次大小,如每次 1000 条,避免 SQL 过长);
  • 更新:批量更新优先用 CASE WHEN 或分批更新,避免大事务。
  1. 动态更新 / 插入
  • <set>(更新)/<trim>(插入)标签,避免多余的逗号;
  • 非空判断:<if test="username != null and username != ''">,防止更新 / 插入空值。
  1. 性能优化
  • 关闭自动提交:批量操作时,设置 SqlSession 为手动提交(session.commit()),减少事务开销;
  • 预编译:默认 statementType=PREPARED,复用预编译语句,提升性能;
  • 超时设置:批量操作指定 timeout(如 60 秒),避免长时间阻塞。
  1. 数据一致性
  • 增删改后必须提交事务(session.commit()),否则数据仅在事务内可见;
  • 避免长事务:批量操作拆分小批次,减少锁表时间。

4.sql 片段

基础语法

定义 SQL 片段
<sql id="userColumns">
    id, username, password, create_time
</sql>

<!-- 带参数的 SQL 片段(支持动态替换) -->
<sql id="tableWithAlias">
    ${alias}.id, ${alias}.username
</sql>
引用 SQL 片段(<include>
<!-- 基础引用 -->
<select id="selectUser" resultType="User">
    select <include refid="userColumns"/>
    from user
    where id = #{id}
</select>

<!-- 带参数引用(动态替换片段中的变量) -->
<select id="selectJoinUser" resultType="map">
    select
        <include refid="tableWithAlias">
            <property name="alias" value="t1"/>
        </include>,
        <include refid="tableWithAlias">
            <property name="alias" value="t2"/>
        </include>
    from user t1
    left join user t2 on t1.id = t2.parent_id
</select>

<!-- 嵌套引用(片段中引用其他片段) -->
<sql id="baseSelect">
    select <include refid="userColumns"/>
    from <include refid="tableName"/>
</sql>
<sql id="tableName">${prefix}_user</sql>

<select id="selectByPrefix" resultType="User">
    <include refid="baseSelect">
        <property name="prefix" value="test"/>
    </include>
    where id = #{id}
</select>

参数映射

简单类型参数(int/String 等)
<!-- 参数名可任意(单参数时),推荐与方法参数名一致 -->
<select id="selectUserById" resultType="User">
    select * from user where id = #{id}
</select>

<!-- 接口方法 -->
User selectUserById(int id);
复杂类型参数(JavaBean/Map)
<!-- 直接引用对象属性名(支持多级属性,如 user.address.id) -->
<insert id="insertUser" parameterType="User">
    insert into user (id, username, password)
    values (#{id}, #{username}, #{password})
</insert>

<!-- 接口方法 -->
int insertUser(User user);
多参数(@Param 注解)
<!-- 用 @Param 指定的参数名引用 -->
<select id="selectUserByCondition" resultType="User">
    select * from user
    where username like #{username} and age > #{age}
</select>

<!-- 接口方法 -->
List<User> selectUserByCondition(@Param("username") String username, @Param("age") int age);

最佳实践

  1. 参数处理
  • 优先使用 #{}:所有参数值传递场景(条件、插入、更新)必须用 #{}
  • 谨慎使用 ${}:仅用于表名 / 列名等元数据,且必须做参数白名单校验;
  • NULL 值处理:为可能为 NULL 的参数指定 jdbcType(如 #{name,jdbcType=VARCHAR});
  • 多参数:推荐用 @Param 注解明确参数名,提高可读性。
  1. SQL 片段
  • 提取高频重复片段:如通用列名、通用 WHERE 条件(如 del_flag = 0);
  • 避免过度拆分:片段粒度不宜过小(如仅拆分单个列),否则增加配置复杂度;
  • 动态参数:片段内使用 ${} 接收参数时,确保参数由后端控制,非用户输入。
  1. 安全规范
  • 禁止将用户输入直接传入 ${}:如前端传的排序字段、表名等,必须后端校验;
  • 敏感操作(如删表、改结构):禁止使用 ${} 拼接,直接写死 SQL;
  • 批量操作:用 foreach + #{},避免 ${} 拼接 IN 条件。

常见问题

  1. #{} 和 ${} 的区别?
  • #{}:预编译,参数替换为 ?,自动转义,防 SQL 注入,用于参数值;
  • ${}:直接字符串拼接,无转义,有注入风险,用于表名 / 列名等元数据。
  1. 为什么 #{id} 能防止 SQL 注入?
  • MyBatis 会将 #{id} 解析为 PreparedStatement 的占位符 ?,参数值通过 setXxx() 方法传入,JDBC 会自动对特殊字符(如 ';)转义,避免恶意 SQL 拼接。
  1. 什么时候必须用 ${}?
  • 需要动态替换 SQL 元数据时(表名、列名、排序字段、分组字段),如 order by ${sortColumn}

5.动态sql

if:基础条件判断

作用

根据条件动态拼接 SQL 片段(最常用,适用于多条件可选查询 / 更新)

语法 & 示例

<!-- 多条件可选查询 -->
<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = 'ACTIVE'
  <!-- 条件:title不为空时拼接 -->
  <if test="title != null and title != ''">
    AND title like #{title}
  </if>
  <!-- 嵌套属性判断 -->
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

关键注意

  • test 属性:OGNL 表达式,支持 !=/==/and/or、属性嵌套(author.name)、空值判断(null/''
  • 单独使用易出现 SQL 语法错误(如多余的 AND),需配合 where/trim 使用

choose (when, otherwise):分支选择

作用

多条件互斥选择(仅执行第一个满足条件的 when,无满足则执行 otherwise

语法 & 示例

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = 'ACTIVE'
  <choose>
    <!-- 第一个满足的条件执行 -->
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <!-- 所有when都不满足时执行 -->
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

适用场景

  • 多条件 “二选一 / 多选一” 场景(如:按标题查 → 按作者查 → 查精选)
  • 避免多个 if 同时满足导致的冗余条件

trim (where/set):语法修正

where:自动处理 WHERE 子句冗余

作用

  • 仅当子元素有内容时,才插入 WHERE 关键字
  • 自动移除子句开头的 AND/OR(解决 if 拼接的语法错误)

示例

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">state = #{state}</if>
    <if test="title != null">AND title like #{title}</if>
    <if test="author != null">AND author_name like #{author.name}</if>
  </where>
</select>

等价自定义 trim

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  <!-- 内部if条件 -->
</trim>
  • prefix:添加前缀(如 WHERE
  • prefixOverrides:移除前缀冗余内容(AND /OR 后需加空格,避免误匹配)
set:动态更新字段

作用

  • 仅当子元素有内容时,插入 SET 关键字
  • 自动移除字段末尾的逗号(解决 if 拼接更新字段的语法错误)

示例

<update id="updateAuthorIfNecessary">
  update Author
  <set>
    <if test="username != null">username=#{username},</if>
    <if test="password != null">password=#{password},</if>
    <if test="email != null">email=#{email}</if>
  </set>
  where id=#{id}
</update>

等价自定义 trim

<trim prefix="SET" suffixOverrides=",">
  <!-- 内部if条件 -->
</trim>
  • suffixOverrides:移除后缀冗余内容(如逗号)
通用 trim:自定义拼接规则
属性作用示例
prefix给拼接内容加前缀prefix="WHERE"
suffix给拼接内容加后缀suffix="ORDER BY id"
prefixOverrides移除前缀冗余字符prefixOverrides="AND "
suffixOverrides移除后缀冗余字符suffixOverrides=","

foreach:集合遍历(高频)

作用

遍历集合 / 数组 / Map,常用于构建 IN 条件、批量插入 / 更新

核心属性

属性作用
collection集合参数名(List=list、数组 = array、Map=key、@Param 指定名)
item遍历的单个元素别名
index索引(List / 数组 = 下标、Map=key)
open遍历内容的开头字符(如 (
separator元素之间的分隔符(如 ,
close遍历内容的结尾字符(如 )
nullable是否允许集合为空(true = 空时不拼接,避免 SQL 错误)

典型示例

(1)IN 条件查询

<select id="selectPostIn" resultType="Post">
  SELECT * FROM POST P
  <where>
    <foreach item="id" index="idx" collection="list"
             open="ID in (" separator="," close=")" nullable="true">
      #{id}
    </foreach>
  </where>
</select>

(2)批量插入

<insert id="batchInsertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username, password) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password})
  </foreach>
</insert>

(3)遍历 Map

<select id="selectByMap" resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <foreach item="value" index="key" collection="params">
      AND ${key} = #{value}
    </foreach>
  </where>
</select>

bind:自定义变量

作用

在 OGNL 上下文创建自定义变量,统一处理参数(如模糊查询的 % 拼接)

示例

<select id="selectBlogsLike" resultType="Blog">
  <!-- 统一拼接模糊查询的%,避免MySQL/Oracle函数差异 -->
  <bind name="pattern" value="'%' + title + '%'" />
  SELECT * FROM BLOG WHERE title LIKE #{pattern}
</select>

优势

  • 替代 concat 函数(MySQL:concat('%', title, '%'),Oracle:'%' || title || '%'),适配多数据库

script:注解中使用动态 SQL

作用

在注解式 Mapper 中嵌入 XML 风格的动态 SQL

示例

@Update({
  "<script>",
  "update Author",
  "  <set>",
  "    <if test='username != null'>username=#{username},</if>",
  "    <if test='password != null'>password=#{password},</if>",
  "  </set>",
  "where id=#{id}",
  "</script>"
})
void updateAuthorValues(Author author);

多数据库适配:_databaseId 变量

作用

结合 databaseIdProvider,为不同数据库生成适配的 SQL

示例

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'mysql'">
      select LAST_INSERT_ID()
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

(四)缓存

MyBatis 缓存分为 一级缓存(本地会话缓存)二级缓存(全局缓存)

一级缓存(默认开启)

核心特性:

  • 作用域:SqlSession 级别(一个数据库会话)
  • 默认开启:无需任何配置,MyBatis 自动启用
  • 缓存规则:

    • 同一个 SqlSession 内,相同 SQL 查询 → 先读缓存,不查数据库(注:Spring中每次请求都使用新的 SqlSession)
    • 执行 insert/update/deletecommit/rollback → 清空当前 SqlSession 一级缓存
    • 关闭 SqlSession → 一级缓存失效
  • 存储介质:内存(基于 HashMap 实现)

示例:一级缓存生效 / 失效场景

// 1. 开启会话
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询:查数据库,结果存入一级缓存
User user1 = mapper.selectUserById(1);

// 第二次查询:相同SQL+参数,直接读一级缓存(不查库)
User user2 = mapper.selectUserById(1);

// 执行update:清空一级缓存
mapper.updateUser(user1);
session.commit();

// 第三次查询:缓存已清空,重新查数据库
User user3 = mapper.selectUserById(1);

// 关闭会话:一级缓存销毁
session.close();

注意:

  • 不同 SqlSession 之间的一级缓存相互隔离,无法共享
  • 手动调用 session.clearCache() 可主动清空当前会话的一级缓存

二级缓存(手动开启)

核心特性

  • 作用域:Mapper 命名空间级别(跨 SqlSession 共享)
  • 开启条件:

    1. 全局配置(默认开启,可省略):

      <settings>
          <setting name="cacheEnabled" value="true"/>
      </settings>
    2. 在 Mapper XML 中添加 <cache/> 标签
  • 缓存规则:

    • 同一命名空间下的所有 select 结果存入二级缓存
    • 同一命名空间下的 insert/update/delete 会刷新二级缓存
    • SqlSession 提交(commit)后,一级缓存数据才会写入二级缓存
    • 关闭 SqlSession 后,二级缓存仍保留(跨会话共享)

基础配置

<!-- 最简配置:使用默认参数 -->
<cache/>

<!-- 自定义参数配置 -->
<cache
  eviction="FIFO"        <!-- 清除策略:LRU(默认)/FIFO/SOFT/WEAK -->
  flushInterval="60000"  <!-- 自动刷新间隔(毫秒),默认无(仅操作时刷新) -->
  size="512"             <!-- 缓存最大引用数,默认1024 -->
  readOnly="true"/>      <!-- 只读(默认false):true=返回对象实例,false=返回对象拷贝 -->

关键参数详解

参数取值 / 说明
evictionLRU(默认):移除最久未使用;FIFO:先进先出;SOFT:软引用;WEAK:弱引用
flushInterval正整数 = 自动刷新间隔(ms);默认 = null = 仅执行增删改 / 手动刷新时清空
size正整数 = 缓存最大对象数(注意内存占用);默认 = 1024
readOnlytrue:只读缓存(返回对象实例,性能高,不可修改);false(默认):可读写(返回拷贝,安全)

二级缓存生效流程

graph TD
    A[SqlSession1 查询] --> B{一级缓存是否存在?}
    B -->|否| C[查数据库]
    C --> D[结果存入一级缓存]
    D --> E[SqlSession1 commit/close]
    E --> F[一级缓存数据写入二级缓存]
    F --> G[SqlSession2 查询]
    G --> H{一级缓存是否存在?}
    H -->|否| I[查二级缓存]
    I -->|存在| J[返回缓存数据]
    I -->|不存在| C

语句级缓存控制(覆盖默认行为)

通过 flushCacheuseCache 属性,自定义单条 SQL 的缓存规则:

标签默认配置作用
<select>flushCache="false" useCache="true"不刷新缓存,结果存入缓存
<insert>/<update>/<delete>flushCache="true"执行后刷新(清空)当前命名空间的二级缓存

示例:自定义语句缓存规则

<!-- 结果不存入缓存(如实时性要求高的查询) -->
<select id="selectUserByPhone" flushCache="false" useCache="false">
    select * from user where phone = #{phone}
</select>

<!-- 查询时强制刷新缓存(清空后再查) -->
<select id="selectLatestOrder" flushCache="true" useCache="true">
    select * from order where create_time = (select max(create_time) from order)
</select>

<!-- 更新时不刷新缓存(特殊场景) -->
<update id="updateUserRemark" flushCache="false">
    update user set remark = #{remark} where id = #{id}
</update>

缓存共享(cache-ref)

  1. 作用

多个 Mapper 命名空间共享同一个缓存配置 / 实例(避免重复缓存)

  1. 配置方式
<!-- UserMapper.xml(主缓存) -->
<cache eviction="LRU" size="1024"/>

<!-- OrderMapper.xml(引用UserMapper的缓存) -->
<cache-ref namespace="com.mapper.UserMapper"/>
  • 引用后,OrderMapper 的缓存与 UserMapper 共用,增删改会互相刷新

自定义缓存(实现 Cache 接口)

  1. 适用场景
  • 集成第三方缓存(如 Redis、Ehcache)
  • 自定义缓存逻辑(如持久化、分布式缓存)
  1. 实现步骤

步骤 1:实现 Cache 接口

public class MyRedisCache implements Cache {
    private final String id; // 缓存ID(命名空间)
    
    // 必须提供带String参数的构造器
    public MyRedisCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() { return id; }

    @Override
    public void putObject(Object key, Object value) {
        // 实现:将key-value存入Redis
    }

    @Override
    public Object getObject(Object key) {
        // 实现:从Redis获取key对应的值
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        // 实现:删除Redis中的key
        return null;
    }

    @Override
    public void clear() {
        // 实现:清空当前命名空间的缓存
    }

    @Override
    public int getSize() {
        // 实现:返回缓存对象数量
        return 0;
    }
}

步骤 2:配置自定义缓存

<!-- Mapper XML 中引用 -->
<cache type="com.cache.MyRedisCache">
    <!-- 自定义属性(MyBatis自动注入) -->
    <property name="host" value="127.0.0.1"/>
    <property name="port" value="6379"/>
</cache>

步骤 3(可选):初始化方法(3.4.2+)

实现 InitializingObject 接口,添加初始化逻辑:

public class MyRedisCache implements Cache, InitializingObject {
    @Override
    public void initialize() throws Exception {
        // 初始化Redis连接等操作
    }
}

最佳实践

  1. 一级缓存
  • 无需额外配置,利用默认行为即可
  • 注意:长会话中执行增删改后,一级缓存会清空,避免脏读
  1. 二级缓存
  • 适用场景:查询频率高、修改频率低的静态数据(如字典、配置表)
  • 禁用场景:实时性要求高的数据(如订单、库存)、多表关联查询(易导致脏数据)
  • 多表关联查询时,建议将关联的 Mapper 通过 cache-ref 共享缓存,避免数据不一致
  • 分布式系统中,优先使用 Redis/Ehcache 等分布式缓存(自定义 Cache 实现),而非默认二级缓存
  1. 避坑要点
  • 二级缓存依赖序列化:缓存的 Java 对象必须实现 Serializable 接口(否则报错)
  • 多表联查时,若关联表的 Mapper 未共享缓存,修改关联表数据会导致二级缓存脏读
  • 只读缓存(readOnly=true)仅适用于查询场景,修改缓存对象会引发线程安全问题

(五)日志

  1. 日志适配规则:MyBatis 内置日志工厂按「SLF4J → Commons Logging → Log4j2 → Log4j(废弃)→ JDK logging」顺序自动选择日志实现,无适配则禁用日志。
  2. 手动指定日志实现:若默认适配不符合需求(如应用服务器自带 Commons Logging 覆盖 Log4j),可通过配置强制指定:

    <!-- mybatis-config.xml -->
    <settings>
      <setting name="logImpl" value="LOG4J2"/> <!-- 可选值:SLF4J/LOG4J2/JDK_LOGGING/STDOUT_LOGGING 等 -->
    </settings>

    或代码指定(需在 MyBatis 初始化前调用):

    LogFactory.useLog4J2Logging(); // 强制使用 Log4j2
  3. 配置思路

    • 依赖:添加对应日志框架的 jar 包(如 Logback/Log4j2);
    • 配置文件:在类路径下创建日志配置文件(如 logback.xml/log4j2.xml);
    • 粒度控制:按「包 / 映射器 / 具体语句」配置日志级别,核心级别:

      • TRACE:打印 SQL + 参数 + 结果;
      • DEBUG:仅打印 SQL + 参数(屏蔽庞大结果);
      • ERROR:仅打印异常(生产环境常用)。
  4. 常用配置示例(以 Logback 为例)

    步骤 1:添加依赖(Maven)

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.4.x</version>
    </dependency>

    步骤 2:配置文件(logback.xml)

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
      <!-- 控制台输出 -->
      <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder><pattern>%5level [%thread] - %msg%n</pattern></encoder>
      </appender>
    
      <!-- 粒度1:整个映射器包(打印所有语句的详细日志) -->
      <logger name="org.mybatis.example" level="trace"/>
      <!-- 粒度2:单个映射器接口/XML命名空间 -->
      <logger name="org.mybatis.example.BlogMapper" level="debug"/>
      <!-- 粒度3:具体SQL语句 -->
      <logger name="org.mybatis.example.BlogMapper.selectBlog" level="trace"/>
    
      <!-- 根日志:仅打印错误 -->
      <root level="error">
        <appender-ref ref="stdout"/>
      </root>
    </configuration>
  5. 其他框架快速配置
日志框架依赖(Maven)配置文件核心配置(示例)
Log4j2log4j-core + log4j-apilog4j2.xml同 Logback,标签为 <Configuration>/<Loggers>
Log4j(废弃)log4j:log4j:1.2.17log4j.propertieslog4j.logger.org.mybatis.example.BlogMapper=TRACE
JDK Logging无需额外依赖logging.propertiesorg.mybatis.example.BlogMapper=FINER
分类: Java后端 标签: Mybatis

评论

暂无评论数据

暂无评论数据

目录