目录
5.2 二级缓存(SqlSessionFactory 级别,需手动开启)
一、引言
在上一篇文章《Mybatis 入门》里,我们掌握了 Mybatis 最基础的用法:通过 SqlSessionFactory 加载配置、用 Mapper 接口和 XML 映射文件完成对数据库的增删改查。但是,实际业务中我们很快会碰到三个麻烦:
- 查询条件不固定——有时按姓名查,有时按年龄查,有时两者都要,总不能为每种组合都写一条 SQL;
- 表与表之间存在关联关系——用户和订单一对多、订单和商品多对多,简单的
resultType已经不够用了; - 每次查询都去数据库,性能上能不能优化?
这篇文章就围绕这几个痛点,把 Mybatis 的进阶能力讲透:动态 SQL、resultMap 高级映射、注解开发、以及一级/二级缓存。
建议先看完前面的《Mybatis 入门》,本文示例都基于同一套
t_user、t_order表结构。
二、动态 SQL
动态 SQL 是 Mybatis 最强大的特性之一,它让我们可以根据传入参数的条件,动态拼接 SQL 语句,避免大量冗余的硬编码。
2.1 if 标签:条件拼接
最常见的场景:按条件查询用户,哪个字段有值就用哪个字段。
xml
<select id="findUserByCondition" parameterType="map" resultType="User">
SELECT id, name, age, email
FROM t_user
WHERE 1 = 1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>
注意:WHERE 1 = 1 是为了方便拼 AND,但这样写不够优雅,所以有了下面的 where 标签。
2.2 where 标签:自动处理 AND/OR
<where> 标签会自动去掉开头的 AND 或 OR,并且当内部条件都不成立时,不会生成 WHERE 关键字。
xml
<select id="findUserByCondition" parameterType="map" resultType="User">
SELECT id, name, age, email
FROM t_user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
2.3 choose/when/otherwise:多选一
相当于 Java 里的 switch-case,只取第一个满足的条件。
xml
<select id="findUser" parameterType="map" resultType="User">
SELECT id, name, age, email FROM t_user
<where>
<choose>
<when test="name != null and name != ''">
AND name = #{name}
</when>
<when test="age != null">
AND age = #{age}
</when>
<otherwise>
AND age > 18
</otherwise>
</choose>
</where>
</select>
2.4 set 标签:动态更新
<set> 用于 UPDATE 语句,会自动去掉末尾多余的逗号。
xml
<update id="updateUser" parameterType="User">
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>
2.5 foreach 标签:批量操作
这是最常用的,比如批量删除、批量插入。
xml
<!-- 批量删除:WHERE id IN (1,2,3) -->
<delete id="deleteByIds" parameterType="list">
DELETE FROM t_user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list">
INSERT INTO t_user (name, age, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
2.6 SQL 片段:抽取复用
把重复的 SQL 抽成 <sql> 片段,用 <include> 引入。
xml
<sql id="userColumns">
id, name, age, email
</sql>
<select id="findAll" resultType="User">
SELECT <include refid="userColumns"/> FROM t_user
</select>
三、resultMap 高级映射
当查询结果列名和实体属性名不一致,或者涉及关联查询(一对一、一对多)时,就需要用 resultMap 手动指定映射关系。
3.1 列名与属性名不一致
数据库里是 user_name,实体里是 userName,直接 resultType 会映射不上(值为 null)。用 resultMap 解决:
xml
<resultMap id="userMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="age" column="age"/>
<result property="email" column="email"/>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT id, user_name, age, email FROM t_user
</select>
小技巧:如果只是命名风格差异(下划线转驼峰),也可以在全局配置里开启
mapUnderscoreToCamelCase,就不用每条都写 resultMap 了。
3.2 一对一关联(association)
场景:订单属于某个用户。查询订单时,把对应的用户对象也一起查出来。
xml
<resultMap id="orderMap" type="com.example.entity.Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
<!-- 一对一:一个订单对应一个用户 -->
<association property="user" javaType="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="email"/>
</association>
</resultMap>
<select id="findOrderWithUser" resultMap="orderMap">
SELECT o.id, o.order_no, o.amount, u.id AS user_id, u.name AS user_name, u.email
FROM t_order o
LEFT JOIN t_user u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
3.3 一对多关联(collection)
场景:一个用户有多个订单。查询用户时,把该用户的所有订单查出来。
xml
<resultMap id="userWithOrdersMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<!-- 一对多:一个用户对应多个订单 -->
<collection property="orders" ofType="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.id, u.name, u.email, o.id AS order_id, o.order_no, o.amount
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
重点区分:一对一用
association,一对多用collection。
四、注解开发
对于简单的 SQL,写 XML 映射文件略显繁琐,Mybatis 提供了注解方式,直接在 Mapper 接口上写 SQL。
java
public interface UserMapper {
@Select("SELECT id, name, age, email FROM t_user WHERE id = #{id}")
User findById(Integer id);
@Insert("INSERT INTO t_user(name, age, email) VALUES(#{name}, #{age}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id") // 回填自增主键
int insert(User user);
@Update("UPDATE t_user SET name = #{name}, age = #{age} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM t_user WHERE id = #{id}")
int delete(Integer id);
// 多参数必须用 @Param 指定名字
@Select("SELECT id, name FROM t_user WHERE name = #{name} AND age = #{age}")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
}
注解开发还支持动态 SQL,用 <script> 包裹即可,但复杂场景仍然建议用 XML 更清晰:
java
@Select("<script>" +
"SELECT * FROM t_user" +
"<where>" +
"<if test='name != null'>AND name = #{name}</if>" +
"</where>" +
"</script>")
List<User> findByCondition(User user);
一句话原则:简单 SQL 用注解,复杂/动态 SQL 用 XML,两者可以混用。
五、Mybatis 缓存机制
Mybatis 内置了两级缓存,理解它们对性能调优很有帮助。
5.1 一级缓存(默认开启,SqlSession 级别)
一级缓存作用于同一个 SqlSession 内。同一个会话中执行相同的查询,第二次会直接返回缓存,不再查库。
java
SqlSession session = factory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User u1 = mapper.findById(1); // 第一次:查数据库
User u2 = mapper.findById(1); // 第二次:命中一级缓存,不查库
System.out.println(u1 == u2); // true,同一个对象
一级缓存会在 SqlSession 执行 增删改 操作、或调用 commit()、close() 时被清空。
5.2 二级缓存(SqlSessionFactory 级别,需手动开启)
二级缓存是跨 SqlSession 共享的。开启步骤:
第一步:全局配置开启二级缓存
xml
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
第二步:在 Mapper 映射文件里加 <cache/> 标签
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
...
</mapper>
第三步:实体类实现 Serializable 接口
java
public class User implements Serializable {
private Integer id;
private String name;
// getter/setter 省略
}
注意:二级缓存默认对所有查询都生效,但多表关联、数据频繁变动的场景不建议用,容易造成脏数据。需要精确控制时可以用
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>配置淘汰策略。
六、总结
这篇文章我们把 Mybatis 从「会用」提升到了「用好」的层次,核心内容可以浓缩成四句话:
- 动态 SQL 解决「条件不固定」的问题:
if、where、set、choose、foreach、sql片段各司其职; - resultMap 解决「列名不一致」和「关联查询」的问题:一对一用
association,一对多用collection; - 注解开发 让简单 CRUD 更简洁,复杂动态 SQL 仍建议回归 XML;
- 一级缓存默认开启、作用在单个
SqlSession;二级缓存需手动开启、跨会话共享,但要警惕脏数据。
掌握了这些,应对绝大多数企业级项目的持久层开发已经绰绰有余。下一篇(也是本系列的最后一篇),我会带大家从零搭建一个基于 SpringBoot 的图书管理系统,把前面六篇文章的知识——SpringBoot 自动装配、SpringMVC 的 Controller、IoC/DI、yml 配置、Mybatis 的入门与进阶——全部串联起来,敬请期待。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_93705143/article/details/166601573




