MyBatis進(jìn)階教程之動態(tài)SQL、關(guān)聯(lián)查詢與緩存
一、引言:中級開發(fā)的 MyBatis “卡殼” 時刻
做開發(fā)半年到兩年的你,是不是常遇到這些問題:
- 多條件查詢時,寫了一堆if-else拼接 SQL,還總因多了個AND或逗號報語法錯;
- 查訂單列表要關(guān)聯(lián)用戶、商品表,結(jié)果要么字段映射混亂,要么查出來數(shù)據(jù)重復(fù);
- 同一頁面頻繁刷新,每次都查數(shù)據(jù)庫,接口響應(yīng)慢得被測試吐槽……
這些 “卡殼” 場景,本質(zhì)是沒掌握 MyBatis 的進(jìn)階能力。本文會從動態(tài) SQL(解決復(fù)雜條件拼接) 、關(guān)聯(lián)查詢(處理多表數(shù)據(jù)) 、緩存機制(優(yōu)化重復(fù)查詢) 三個維度,用 “場景 + 代碼 + 原理” 的方式拆解,幫你把 MyBatis 從 “能用” 升級到 “用好”,應(yīng)對 80% 的中級開發(fā)場景。
二、動態(tài) SQL 深度解析:告別 “SQL 拼接地獄”
動態(tài) SQL 是 MyBatis 的 “靈魂功能”,能根據(jù)條件自動拼接 SQL 片段,避免手動拼接的冗余與錯誤。下面 6 個核心標(biāo)簽,從基礎(chǔ)到進(jìn)階逐個拆解,每個標(biāo)簽都附實戰(zhàn)場景與完整代碼。
1. if 標(biāo)簽:最常用的 “條件判斷”
場景:多條件查詢(如電商列表頁,用戶可輸入用戶名、選擇價格區(qū)間,非必填條件)
作用:滿足test表達(dá)式時,才拼接標(biāo)簽內(nèi)的 SQL 片段。
代碼示例:根據(jù)用戶名和年齡查詢用戶
<!-- UserMapper.xml -->
<select id="selectUserByCondition" resultType="User">
SELECT id, username, age, email
FROM user
WHERE 1=1 <!-- 占位符,避免后續(xù)if都不滿足時出現(xiàn)“WHERE”后無條件的語法錯 -->
<!-- test表達(dá)式:判斷參數(shù)是否非空,支持OGNL語法(如username != null and username != '') -->
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%') <!-- 模糊查詢拼接 -->
</if>
<if test="age != null">
AND age > #{age} <!-- 年齡大于傳入值 -->
</if>
</select>
// UserMapper.java接口
List<User> selectUserByCondition(@Param("username") String username, @Param("age") Integer age);
// 測試代碼
// 場景1:只傳username,SQL會拼接“AND username LIKE '%張%'”
List<User> user1 = userMapper.selectUserByCondition("張", null);
// 場景2:只傳age,SQL會拼接“AND age > 25”
List<User> user2 = userMapper.selectUserByCondition(null, 25);
關(guān)鍵注意點:
test表達(dá)式中,參數(shù)名要和@Param或?qū)嶓w類屬性一致(如參數(shù)是User對象,就用user.age);
為什么加WHERE 1=1?如果所有if都不滿足,SQL 會變成SELECT … FROM user WHERE,語法錯誤;加了1=1,不滿足時就是WHERE 1=1,合法。
2. where 標(biāo)簽:自動 “收拾” 多余的 AND/OR
場景:替代WHERE 1=1,更優(yōu)雅地處理條件拼接
作用:自動去除標(biāo)簽內(nèi) SQL 片段開頭的AND或OR,避免語法錯誤。
代碼示例:優(yōu)化上面的多條件查詢
<select id="selectUserByCondition" resultType="User">
SELECT id, username, age, email
FROM user
<!-- where標(biāo)簽自動處理多余的AND -->
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%') <!-- 開頭的AND會被自動去除 -->
</if>
<if test="age != null">
AND age > #{age}
</if>
</where>
</select>
對比優(yōu)勢:
無需寫WHERE 1=1,代碼更簡潔;
即使第一個if不滿足,第二個if開頭的AND也會被處理(比如只傳 age,SQL 是WHERE age > 25,無多余 AND)。
3. choose(when/otherwise)標(biāo)簽:“二選一” 的分支邏輯
場景:互斥條件查詢(如電商訂單查詢,“按訂單號查” 和 “按用戶 ID 查” 只能選一個,不能同時生效)
作用:類似 Java 的switch-case,只執(zhí)行第一個滿足條件的when,都不滿足則執(zhí)行otherwise。
代碼示例:訂單查詢(訂單號優(yōu)先,無則按用戶 ID 查)
<select id="selectOrder" resultType="Order">
SELECT id, order_no, user_id, amount
FROM `order`
<where>
<choose>
<!-- 第一個滿足的條件生效:有訂單號則按訂單號查 -->
<when test="orderNo != null and orderNo != ''">
AND order_no = #{orderNo}
</when>
<!-- 訂單號為空,按用戶ID查 -->
<when test="userId != null">
AND user_id = #{userId}
</when>
<!-- 都為空,查近7天的訂單(默認(rèn)條件) -->
<otherwise>
AND create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
</otherwise>
</choose>
</where>
</select>
實戰(zhàn)提醒:
choose是 “互斥” 邏輯,和多個if的 “并列” 邏輯區(qū)分開(多個if會同時生效,choose只生效一個);
otherwise可選,無默認(rèn)條件時可省略,但建議加上,避免無條件查詢?nèi)怼?/p>
4. set 標(biāo)簽:動態(tài)更新時 “去掉多余逗號”
場景:部分字段更新(如用戶編輯,只改用戶名和郵箱,不改年齡)
作用:自動去除標(biāo)簽內(nèi) SQL 片段結(jié)尾的逗號,避免UPDATE … SET username=‘xxx’, WHERE …的語法錯。
代碼示例:更新用戶信息(只更非空字段)
<update id="updateUserSelective">
UPDATE user
<!-- set標(biāo)簽自動處理多余逗號 -->
<set>
<if test="username != null and username != ''">
username = #{username}, <!-- 結(jié)尾的逗號會被自動去除 -->
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null and email != ''">
email = #{email}
</if>
</set>
WHERE id = #{id} <!-- 必須傳id,否則更新全表! -->
</update>
避坑指南:
千萬不要漏寫WHERE id = #{id},否則會更新表中所有數(shù)據(jù),生產(chǎn)環(huán)境必出事故;
如果所有if都不滿足(沒傳任何要更新的字段),SQL 會變成UPDATE user SET WHERE id=?,語法錯誤,建議在 Service 層加參數(shù)校驗(至少有一個字段非空)。
5. foreach 標(biāo)簽:遍歷集合的 “批量操作神器”
場景:批量插入、批量刪除、IN查詢(如批量插入 10 個用戶、刪除 ID 為 1,2,3 的用戶、查詢 ID 在 [10,20] 的訂單)
作用:遍歷List/Array/Map,拼接成對應(yīng)的 SQL 片段(如(1,2,3)或VALUES (?,?),(?,?))。
核心屬性解析:

實戰(zhàn)場景 1:批量插入用戶(List 參數(shù))
<insert id="batchInsertUser">
INSERT INTO user (username, age, email)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.username}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 對應(yīng)的Mapper接口 -->
int batchInsertUser(@Param("userList") List<User> userList);
<!-- 測試代碼:插入3個用戶 -->
List<User> userList = new ArrayList<>();
userList.add(new User("李四", 28, "lisi@xxx.com"));
userList.add(new User("王五", 30, "wangwu@xxx.com"));
userMapper.batchInsertUser(userList);
實戰(zhàn)場景 2:批量刪除用戶(Array 參數(shù))
<delete id="batchDeleteUser">
DELETE FROM user
WHERE id IN
<foreach collection="array" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
<!-- 對應(yīng)的Mapper接口 -->
int batchDeleteUser(Integer[] ids);
<!-- 測試代碼:刪除ID為1、2的用戶 -->
Integer[] ids = {1,2};
userMapper.batchDeleteUser(ids);
性能優(yōu)化:
批量插入時,若數(shù)據(jù)量超過 1000 條,建議分批次(如每次 500 條),避免 SQL 語句過長導(dǎo)致數(shù)據(jù)庫執(zhí)行超時;
collection取值易錯:如果 Mapper 接口參數(shù)沒加@Param,List 默認(rèn)用list,Array 默認(rèn)用array;加了@Param,就用@Param指定的名稱(如@Param(“userList”)→collection=“userList”)。
6. trim 標(biāo)簽:自定義 SQL 拼接規(guī)則(萬能標(biāo)簽)
場景:替代where/set,或?qū)崿F(xiàn)更靈活的拼接(如給 SQL 加前綴、后綴,去除特定字符)
作用:通過prefix(前綴)、suffix(后綴)、prefixOverrides(去除開頭字符)、suffixOverrides(去除結(jié)尾字符)自定義規(guī)則。
示例 1:用 trim 替代 where 標(biāo)簽
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
AND age > #{age}
</if>
</trim>
<!-- 效果等同于where標(biāo)簽:開頭加WHERE,去除多余AND/OR -->
示例 2:用 trim 替代 set 標(biāo)簽
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">
username = #{username},
</if>
<if test="email != null">
email = #{email},
</if>
</trim>
<!-- 效果等同于set標(biāo)簽:開頭加SET,去除結(jié)尾逗號 -->
示例 3:自定義拼接(給查詢結(jié)果加固定條件)
<select id="selectUserWithStatus" resultType="User">
SELECT id, username, age
FROM user
<trim prefix="WHERE" prefixOverrides="AND">
<if test="username != null">
AND username LIKE '%${username}%'
</if>
<!-- 強制拼接“status=1”(只查正常用戶) -->
AND status = 1
</trim>
</select>
使用建議:
簡單場景用where/set,復(fù)雜場景用trim;
trim靈活性高,但可讀性稍差,團(tuán)隊協(xié)作時建議統(tǒng)一規(guī)則(如優(yōu)先用where/set)。
三、復(fù)雜關(guān)聯(lián)查詢實現(xiàn):搞定多表數(shù)據(jù)映射
實際項目中,很少只查單表(如查訂單要帶用戶信息,查用戶要帶訂單列表),這就需要關(guān)聯(lián)查詢。下面按 “一對一→一對多→多對多” 的順序,講清實現(xiàn)方式、優(yōu)缺點與性能優(yōu)化。
1. 一對一關(guān)聯(lián):用戶與身份證(一個用戶對應(yīng)一個身份證)
場景:查詢用戶時,同時返回其身份證信息(如用戶表user,身份證表id_card,通過 user.id = id_card.user_id 關(guān)聯(lián))。
兩種實現(xiàn)方式:resultType(用 DTO 接收) vs resultMap(用 association 標(biāo)簽配置)。
方式 1:resultType(簡單場景,用 DTO 接收關(guān)聯(lián)結(jié)果)
步驟 1:創(chuàng)建 DTO 類(包含用戶和身份證的所有字段)
// UserWithIdCardDTO.java
@Data
public class UserWithIdCardDTO {
// 用戶表字段
private Integer userId;
private String username;
private Integer age;
// 身份證表字段(加前綴區(qū)分,避免字段名沖突)
private Integer cardId;
private String cardNo; // 身份證號
private String issueOrg; // 發(fā)證機關(guān)
}
步驟 2:編寫 Mapper 接口與 XML
// UserMapper.java
List<UserWithIdCardDTO> selectUserWithIdCard();
<select id="selectUserWithIdCard" resultType="UserWithIdCardDTO">
SELECT
u.id AS userId,
u.username,
u.age,
ic.id AS cardId,
ic.card_no AS cardNo,
ic.issue_org AS issueOrg
FROM user u
LEFT JOIN id_card ic ON u.id = ic.user_id <!-- 左連接,確保沒有身份證的用戶也能查到 -->
</select>
方式 2:resultMap(復(fù)雜場景,用 association 標(biāo)簽)
步驟 1:創(chuàng)建實體類(User 包含 IdCard 屬性)
// IdCard.java(身份證實體)
@Data
public class IdCard {
private Integer id;
private String cardNo;
private String issueOrg;
private Integer userId; // 關(guān)聯(lián)用戶ID
}
// User.java(用戶實體,包含IdCard)
@Data
public class User {
private Integer id;
private String username;
private Integer age;
private IdCard idCard; // 一對一關(guān)聯(lián):用戶有一個身份證
}
步驟 2:編寫 resultMap 與 SQL
<!-- 定義resultMap:關(guān)聯(lián)User和IdCard -->
<resultMap id="UserWithIdCardMap" type="User">
<!-- 用戶表字段映射 -->
<id column="user_id" property="id"/> <!-- id標(biāo)簽:主鍵字段,提高映射效率 -->
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- association標(biāo)簽:配置一對一關(guān)聯(lián) -->
<association property="idCard" javaType="IdCard"> <!-- javaType:關(guān)聯(lián)對象的類型 -->
<id column="card_id" property="id"/>
<result column="card_no" property="cardNo"/>
<result column="issue_org" property="issueOrg"/>
</association>
</resultMap>
<!-- 關(guān)聯(lián)查詢SQL -->
<select id="selectUserWithIdCard2" resultMap="UserWithIdCardMap">
SELECT
u.id AS user_id,
u.username,
u.age,
ic.id AS card_id,
ic.card_no,
ic.issue_org
FROM user u
LEFT JOIN id_card ic ON u.id = ic.user_id
</select>
兩種方式對比:

延遲加載配置:
一對一關(guān)聯(lián)時,若默認(rèn)不查身份證(需要時才查),可開啟延遲加載(減輕數(shù)據(jù)庫壓力):
全局配置(application.yml):
mybatis:
configuration:
lazy-loading-enabled: true # 開啟全局延遲加載
aggressive-lazy-loading: false # 關(guān)閉“積極加載”(只加載需要的屬性)
修改 association 標(biāo)簽,添加select和column:
<association
property="idCard"
javaType="IdCard"
select="com.example.mapper.IdCardMapper.selectIdCardByUserId" <!-- 關(guān)聯(lián)查詢的Mapper方法 -->
column="user_id"> <!-- 傳遞給關(guān)聯(lián)方法的參數(shù)(用戶ID) -->
</association>
編寫 IdCardMapper 的查詢方法:
// IdCardMapper.java
IdCard selectIdCardByUserId(Integer userId);
<select id="selectIdCardByUserId" resultType="IdCard">
SELECT * FROM id_card WHERE user_id = #{userId}
</select>
效果:查詢用戶時,只查user表;當(dāng)調(diào)用user.getIdCard()時,才會執(zhí)行id_card表的查詢(按需加載)。
2. 一對多關(guān)聯(lián):用戶與訂單(一個用戶對應(yīng)多個訂單)
場景:查詢用戶時,同時返回其所有訂單(如用戶表user,訂單表order,通過user.id = order.user_id關(guān)聯(lián))。
核心標(biāo)簽:collection(配置集合關(guān)聯(lián),對應(yīng)實體類中的List屬性)。
實現(xiàn)步驟:
創(chuàng)建實體類(User 包含 List)
// Order.java(訂單實體)
@Data
public class Order {
private Integer id;
private String orderNo; // 訂單號
private BigDecimal amount; // 金額
private Integer userId; // 關(guān)聯(lián)用戶ID
}
// User.java(用戶實體,包含訂單列表)
@Data
public class User {
private Integer id;
private String username;
private Integer age;
private List<Order> orderList; // 一對多關(guān)聯(lián):用戶有多個訂單
}
編寫 resultMap 與 SQL
<!-- 定義resultMap:關(guān)聯(lián)User和Order -->
<resultMap id="UserWithOrderMap" type="User">
<!-- 用戶表字段映射 -->
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- collection標(biāo)簽:配置一對多關(guān)聯(lián) -->
<collection
property="orderList" <!-- 實體類中的集合屬性名 -->
ofType="Order" <!-- 集合中元素的類型(注意不是javaType) -->
column="user_id"> <!-- 關(guān)聯(lián)的外鍵字段(用戶ID) -->
<!-- 訂單表字段映射 -->
<id column="order_id" property="id"/> <!-- 訂單主鍵,避免數(shù)據(jù)重復(fù) -->
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<!-- 關(guān)聯(lián)查詢SQL -->
<select id="selectUserWithOrder" resultMap="UserWithOrderMap">
SELECT
u.id AS user_id,
u.username,
u.age,
o.id AS order_id,
o.order_no,
o.amount
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
ORDER BY u.id, o.id <!-- 按用戶ID和訂單ID排序,避免數(shù)據(jù)混亂 -->
</select>
關(guān)鍵注意點:
- ofType vs javaType:collection用ofType指定集合元素類型(如Order),association用javaType指定關(guān)聯(lián)對象類型(如IdCard),別搞混;
- 避免數(shù)據(jù)重復(fù):必須給主表(user)和從表(order)的主鍵加id標(biāo)簽,MyBatis 會通過主鍵判斷是否為同一對象,否則會重復(fù)生成用戶對象;
- 嵌套查詢 vs 嵌套結(jié)果:
上面的示例是 “嵌套結(jié)果”(一次 SQL 聯(lián)表查詢),優(yōu)點是效率高,缺點是 SQL 復(fù)雜;
“嵌套查詢”(先查用戶,再查訂單)類似一對一延遲加載,需配置select屬性,但可能出現(xiàn) “N+1 問題”(查 1 個用戶查 1 次,查 N 個用戶查 N 次訂單,共 N+1 次查詢),數(shù)據(jù)量大時不推薦。
3. 多對多關(guān)聯(lián):用戶與角色(一個用戶有多個角色,一個角色有多個用戶)
場景:查詢用戶時,同時返回其所有角色(需中間表user_role關(guān)聯(lián),三張表:user、role、user_role)。
實現(xiàn)思路:先通過user和user_role聯(lián)表,再關(guān)聯(lián)role,用collection標(biāo)簽映射角色列表。
實現(xiàn)步驟:
創(chuàng)建實體類(User 包含 List)
// Role.java(角色實體)
@Data
public class Role {
private Integer id;
private String roleName; // 角色名(如“管理員”“普通用戶”)
}
// User.java(用戶實體,包含角色列表)
@Data
public class User {
private Integer id;
private String username;
private Integer age;
private List<Role> roleList; // 多對多關(guān)聯(lián):用戶有多個角色
}
編寫 resultMap 與 SQL
<!-- 定義resultMap:關(guān)聯(lián)User和Role -->
<resultMap id="UserWithRoleMap" type="User">
<!-- 用戶表字段映射 -->
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- collection標(biāo)簽:配置多對多關(guān)聯(lián)(角色列表) -->
<collection
property="roleList"
ofType="Role"
column="user_id">
<id column="role_id" property="id"/> <!-- 角色主鍵,避免重復(fù) -->
<result column="role_name" property="roleName"/>
</collection>
</resultMap>
<!-- 多表聯(lián)查SQL:user → user_role → role -->
<select id="selectUserWithRole" resultMap="UserWithRoleMap">
SELECT
u.id AS user_id,
u.username,
u.age,
r.id AS role_id,
r.role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id <!-- 中間表關(guān)聯(lián) -->
LEFT JOIN role r ON ur.role_id = r.id <!-- 角色表關(guān)聯(lián) -->
WHERE u.id = #{userId} <!-- 按用戶ID查詢,避免返回所有用戶 -->
</select>
性能優(yōu)化:
多對多查詢本質(zhì)是 “兩次一對多”(用戶→用戶角色,用戶角色→角色),聯(lián)表時注意索引(user_role.user_id和user_role.role_id加索引);
若只需查詢用戶的角色 ID,可不用關(guān)聯(lián)role表,直接查user和user_role,減少表連接次數(shù)。
四、MyBatis 緩存機制:從 “重復(fù)查庫” 到 “緩存復(fù)用”
緩存是優(yōu)化接口性能的關(guān)鍵 —— 同一數(shù)據(jù)頻繁查詢時,從緩存取比查數(shù)據(jù)庫快 10~100 倍。MyBatis 有兩級緩存,再加上第三方緩存(如 Redis),能覆蓋絕大多數(shù)場景。
一級緩存(SqlSession 級緩存):默認(rèn)開啟,“會話內(nèi)有效”
原理:MyBatis 默認(rèn)在SqlSession(數(shù)據(jù)庫會話)內(nèi)緩存數(shù)據(jù),同一SqlSession執(zhí)行相同的 SQL(相同的 SQL 語句 + 參數(shù)),第一次查數(shù)據(jù)庫并緩存,第二次直接從緩存取,無需查庫。
實戰(zhàn)驗證:一級緩存命中與失效場景
// 測試代碼:同一SqlSession內(nèi)的緩存命中
@Test
public void testFirstLevelCache() {
// 1. 獲取SqlSession(Spring環(huán)境下可通過SqlSessionTemplate獲?。?
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 2. 第一次查詢:查數(shù)據(jù)庫,緩存數(shù)據(jù)
User user1 = userMapper.selectUserById(1);
System.out.println("第一次查詢:" + user1);
// 3. 第二次查詢:相同SQL+參數(shù),從緩存?。ú徊閹欤?
User user2 = userMapper.selectUserById(1);
System.out.println("第二次查詢:" + user2);
System.out.println("是否同一對象:" + (user1 == user2)); // 結(jié)果:true(緩存命中)
// 4. 關(guān)閉SqlSession:緩存清空
sqlSession.close();
// 5. 新的SqlSession:緩存失效,重新查庫
SqlSession newSqlSession = sqlSessionFactory.openSession();
UserMapper newUserMapper = newSqlSession.getMapper(UserMapper.class);
User user3 = newUserMapper.selectUserById(1);
System.out.println("新SqlSession查詢:" + user3);
System.out.println("是否同一對象:" + (user1 == user3)); // 結(jié)果:false(緩存失效)
}
一級緩存失效場景(必須掌握):

實戰(zhàn)提醒:
Spring 整合 MyBatis 時,默認(rèn)每個 Service 方法是一個SqlSession(事務(wù)提交后關(guān)閉),所以一級緩存在 Service 方法內(nèi)有效,跨方法失效;
不要在多線程中共享SqlSession,會導(dǎo)致緩存數(shù)據(jù)被覆蓋,出現(xiàn)數(shù)據(jù)一致性問題(如線程 A 查 id=1,線程 B 查 id=2,緩存可能混亂)。
2. 二級緩存(Mapper 級緩存):手動開啟,“跨會話共享”
原理:二級緩存是Mapper接口級別的緩存(如UserMapper的緩存獨立于OrderMapper),不同SqlSession執(zhí)行同一Mapper的相同 SQL,
可共享緩存,緩存數(shù)據(jù)存儲在Mapper對應(yīng)的Cache對象中。
開啟二級緩存的 3 個步驟:
全局配置開啟(application.yml)
mybatis:
configuration:
cache-enabled: true # 開啟全局二級緩存(默認(rèn)true,可省略,但建議顯式配置)
在 Mapper XML 中添加標(biāo)簽(關(guān)鍵)
<!-- UserMapper.xml:開啟當(dāng)前Mapper的二級緩存 -->
<cache
eviction="LRU" <!-- 緩存回收策略:LRU(最近最少使用) -->
flushInterval="60000" <!-- 緩存刷新間隔:60秒(60000毫秒),到期自動清空 -->
size="1024" <!-- 緩存最大條目:最多存1024條數(shù)據(jù) -->
readOnly="true"/> <!-- 只讀:true(返回緩存對象的引用,性能高);false(返回副本,安全) -->
實體類實現(xiàn)Serializable接口(否則緩存報錯)
// User.java必須實現(xiàn)Serializable,因為二級緩存會序列化數(shù)據(jù)
@Data
public class User implements Serializable {
private Integer id;
private String username;
private Integer age;
// ...其他字段
}
二級緩存核心參數(shù)解析(標(biāo)簽):

二級緩存生效與失效場景:
- 生效:不同SqlSession執(zhí)行同一Mapper的相同 SQL(如UserMapper.selectUserById(1)),第二次從緩存??;
- 失效:
執(zhí)行當(dāng)前Mapper的增刪改操作(會清空該Mapper的二級緩存);
緩存到期(flushInterval時間到)或達(dá)到size上限(觸發(fā)回收策略);
方法上加@Options(useCache = false)(禁用當(dāng)前方法的二級緩存)。
示例:禁用某個方法的二級緩存
// UserMapper.java:查詢用戶列表時,禁用二級緩存(數(shù)據(jù)變化頻繁) @Options(useCache = false) List<User> selectAllUsers();
二級緩存 vs 一級緩存對比:

3. 整合第三方緩存(Redis):解決分布式緩存共享問題
問題:二級緩存默認(rèn)是內(nèi)存緩存,分布式環(huán)境下(多臺服務(wù)器),各服務(wù)器的緩存不共享(如服務(wù)器 A 查了 id=1,服務(wù)器 B 查 id=1 仍需查庫),需用 Redis 實現(xiàn)分布式緩存。
整合步驟(SpringBoot+Redis):
1. 引入依賴(pom.xml)
<!-- Redis依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis-Redis整合依賴(提供RedisCache實現(xiàn)) -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
2. 配置 Redis(application.yml)
spring:
redis:
host: localhost # Redis服務(wù)器地址
port: 6379 # Redis端口
password: # Redis密碼(無則空)
database: 0 # Redis數(shù)據(jù)庫索引(默認(rèn)0)
timeout: 3000 # 連接超時時間(毫秒)
3. 修改 Mapper XML 的標(biāo)簽,指定 Redis 緩存實現(xiàn)
<!-- UserMapper.xml:使用Redis作為二級緩存 -->
<cache
type="org.mybatis.caches.redis.RedisCache" <!-- RedisCache的全類名 -->
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
自定義 Redis 緩存配置(可選,解決默認(rèn)配置問題):
MyBatis-Redis 默認(rèn)緩存時間是永久的,可自定義RedisCache類,設(shè)置過期時間:
// 自定義RedisCache,繼承MyBatis的RedisCache
public class CustomRedisCache extends RedisCache {
// 緩存過期時間:30分鐘(1800000毫秒)
private static final long EXPIRE_TIME = 1800000;
public CustomRedisCache(String id) {
super(id); // 調(diào)用父類構(gòu)造器
}
// 重寫putObject方法,添加過期時間
@Override
public void putObject(Object key, Object value) {
RedisManager redisManager = RedisConfigurationBuilder.getInstance().build();
try (Jedis jedis = redisManager.getResource()) {
jedis.set(Objects.toString(key), SerializeUtil.serialize(value));
jedis.expire(Objects.toString(key), (int) (EXPIRE_TIME / 1000)); // 設(shè)置過期時間(秒)
}
}
}
然后在 Mapper XML 中引用自定義緩存:
<cache type="com.example.cache.CustomRedisCache"/>
五、實戰(zhàn)案例:電商訂單查詢模塊(整合所有進(jìn)階技能)
用一個真實場景,把動態(tài) SQL、關(guān)聯(lián)查詢、緩存串起來,讓你看到 “學(xué)以致用” 的效果。
1. 需求分析
功能:查詢訂單列表,支持多條件篩選(訂單號、用戶 ID、時間范圍、訂單狀態(tài));
關(guān)聯(lián):訂單需顯示用戶信息(一對一)、訂單商品列表(一對多);
性能:查詢結(jié)果緩存,訂單增刪改后清空緩存。
2. 技術(shù)設(shè)計
表結(jié)構(gòu):order(訂單表)、user(用戶表)、order_item(訂單商品表);
核心技能:動態(tài) SQL(多條件篩選)、關(guān)聯(lián)查詢(訂單→用戶、訂單→商品)、二級緩存(緩存訂單列表)。
3. 代碼實現(xiàn)
實體類與 DTO 設(shè)計
// 訂單商品實體(OrderItem.java)
@Data
public class OrderItem implements Serializable {
private Integer id;
private Integer orderId; // 關(guān)聯(lián)訂單ID
private String productName; // 商品名
private Integer quantity; // 數(shù)量
private BigDecimal price; // 單價
}
// 訂單實體(Order.java,包含用戶和商品列表)
@Data
public class Order implements Serializable {
private Integer id;
private String orderNo;
private Integer userId;
private BigDecimal amount;
private Integer status; // 訂單狀態(tài):0-待支付,1-已支付
private Date createTime;
// 關(guān)聯(lián)用戶(一對一)
private User user;
// 關(guān)聯(lián)訂單商品(一對多)
private List<OrderItem> orderItemList;
}
// 訂單查詢DTO(OrderQueryDTO.java,接收前端篩選條件)
@Data
public class OrderQueryDTO {
private String orderNo; // 訂單號(模糊查詢)
private Integer userId; // 用戶ID
private Date startTime; // 開始時間
private Date endTime; // 結(jié)束時間
private Integer status; // 訂單狀態(tài)
}
Mapper 接口與 XML
// OrderMapper.java
public interface OrderMapper {
// 多條件查詢訂單列表(帶關(guān)聯(lián))
List<Order> selectOrderList(OrderQueryDTO queryDTO);
// 新增訂單(用于測試緩存失效)
int insertOrder(Order order);
}
<!-- OrderMapper.xml -->
<!-- 1. 開啟二級緩存(用Redis) -->
<cache type="com.example.cache.CustomRedisCache"/>
<!-- 2. 定義resultMap:關(guān)聯(lián)訂單、用戶、商品 -->
<resultMap id="OrderWithUserAndItemMap" type="Order">
<!-- 訂單表字段 -->
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="amount" property="amount"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<!-- 關(guān)聯(lián)用戶(一對一) -->
<association property="user" javaType="User">
<id column="u_id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
</association>
<!-- 關(guān)聯(lián)訂單商品(一對多) -->
<collection property="orderItemList" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="product_name" property="productName"/>
<result column="quantity" property="quantity"/>
<result column="price" property="price"/>
</collection>
</resultMap>
<!-- 3. 動態(tài)SQL:多條件查詢訂單列表 -->
<select id="selectOrderList" resultMap="OrderWithUserAndItemMap">
SELECT
o.id AS order_id,
o.order_no,
o.user_id,
o.amount,
o.status,
o.create_time,
-- 用戶表字段
u.id AS u_id,
u.username,
u.email,
-- 訂單商品表字段
oi.id AS item_id,
oi.product_name,
oi.quantity,
oi.price
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
<where>
<!-- 動態(tài)條件:訂單號模糊查詢 -->
<if test="orderNo != null and orderNo != ''">
AND o.order_no LIKE CONCAT('%', #{orderNo}, '%')
</if>
<!-- 用戶ID -->
<if test="userId != null">
AND o.user_id = #{userId}
</if>
<!-- 時間范圍:創(chuàng)建時間 >= 開始時間 -->
<if test="startTime != null">
AND o.create_time >= #{startTime}
</if>
<!-- 時間范圍:創(chuàng)建時間 <= 結(jié)束時間 -->
<if test="endTime != null">
AND o.create_time <= #{endTime}
</if>
<!-- 訂單狀態(tài) -->
<if test="status != null">
AND o.status = #{status}
</if>
</where>
ORDER BY o.create_time DESC
</select>
<!-- 4. 新增訂單:執(zhí)行后會清空當(dāng)前Mapper的二級緩存 -->
<insert id="insertOrder" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `order` (order_no, user_id, amount, status, create_time)
VALUES (#{orderNo}, #{userId}, #{amount}, #{status}, #{createTime})
</insert>
Service 層(整合緩存與業(yè)務(wù)邏輯)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 查詢訂單列表(緩存生效)
public List<Order> getOrderList(OrderQueryDTO queryDTO) {
return orderMapper.selectOrderList(queryDTO);
}
// 新增訂單(執(zhí)行后緩存失效)
@Transactional
public void addOrder(Order order) {
order.setCreateTime(new Date());
orderMapper.insertOrder(order);
// 新增訂單后,OrderMapper的二級緩存會自動清空,下次查列表會重新查庫
}
}
效果驗證
第一次調(diào)用getOrderList:查數(shù)據(jù)庫,結(jié)果存入 Redis 緩存;
第二次調(diào)用getOrderList(相同條件):從 Redis 取,接口響應(yīng)時間從 500ms→50ms;
調(diào)用addOrder新增訂單:OrderMapper的二級緩存清空;
第三次調(diào)用getOrderList:重新查數(shù)據(jù)庫,獲取最新訂單列表。
六、性能優(yōu)化與最佳實踐
掌握進(jìn)階技能后,還要知道 “怎么用才更好”,下面是動態(tài) SQL、關(guān)聯(lián)查詢、緩存的實戰(zhàn)優(yōu)化建議。
1. 動態(tài) SQL 優(yōu)化
避免冗余條件:相同的條件判斷(如username != null and username != ‘’)可抽成 SQL 片段,用和復(fù)用:
<!-- 定義SQL片段 -->
<sql id="usernameCondition">
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
</sql>
<!-- 引用片段 -->
<select id="selectUser" resultType="User">
SELECT * FROM user
<where>
<include refid="usernameCondition"/>
<if test="age != null">
AND age > #{age}
</if>
</where>
</select>
批量操作分批次:批量插入 / 刪除超過 1000 條時,分批次處理(如每次 500 條),避免 SQL 過長導(dǎo)致數(shù)據(jù)庫執(zhí)行超時。
2. 關(guān)聯(lián)查詢優(yōu)化
減少表連接數(shù)量:多表聯(lián)查盡量控制在 3 張表以內(nèi),超過 3 張可拆分成多次查詢(用延遲加載);
避免笛卡爾積:聯(lián)表時確保關(guān)聯(lián)條件正確(如 u.id = o.user_id),否則會出現(xiàn)數(shù)據(jù)爆炸(如 100 個用戶 ×100 個訂單 = 10000 條重復(fù)數(shù)據(jù));
用索引優(yōu)化聯(lián)表:關(guān)聯(lián)字段(如order.user_id、user_role.role_id)必須加索引,否則聯(lián)表查詢會全表掃描,性能極差。
3. 緩存優(yōu)化
緩存高頻、少變的數(shù)據(jù):如字典表、商品分類表(更新頻率低),避免緩存高頻變化的數(shù)據(jù)(如訂單表、用戶余額表);
不緩存大對象:如包含大量商品的訂單詳情(數(shù)據(jù)量大,序列化耗時),可只緩存關(guān)鍵信息(如訂單 ID、金額),詳情按需查庫;
分布式緩存用 Redis:單機用二級緩存,分布式環(huán)境必須用 Redis,避免緩存不共享問題;
緩存一致性保障:增刪改操作后,確保緩存被清空(MyBatis 二級緩存會自動清空當(dāng)前 Mapper 的緩存,Redis 需手動或通過自定義緩存處理)。
總結(jié)
到此這篇關(guān)于MyBatis進(jìn)階教程之動態(tài)SQL、關(guān)聯(lián)查詢與緩存的文章就介紹到這了,更多相關(guān)MyBatis動態(tài)SQL、關(guān)聯(lián)查詢與緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
怎樣將一個JAR包添加到Java應(yīng)用程序的Boot?Classpath中
本文文章給大家介紹如何將一個JAR包添加到Java應(yīng)用程序的Boot?Classpath中,本文通過實例代碼給大家介紹的非常詳細(xì),需要的的朋友參考下吧2023-11-11
PropertiesLoaderUtils 出現(xiàn)中文亂碼的解決方式
這篇文章主要介紹了PropertiesLoaderUtils 出現(xiàn)中文亂碼的解決方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀
這篇文章主要介紹了Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀,線程池是一種多線程處理形式,處理過程中將任務(wù)添加到隊列,然后在創(chuàng)建線程后自動啟動這些任務(wù),線程池線程都是后臺線程,需要的朋友可以參考下2023-12-12
try-with-resource優(yōu)雅關(guān)閉io流的方法
這篇文章主要給大家介紹了關(guān)于try-with-resource優(yōu)雅關(guān)閉io流的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
JavaWeb Struts文件上傳功能實現(xiàn)詳解
這篇文章主要為大家詳細(xì)介紹了JavaWeb Struts文件上傳功能實現(xiàn)過程,思路清晰,供大家參考,感興趣的小伙伴們可以參考一下2016-06-06
SSH框架網(wǎng)上商城項目第20戰(zhàn)之在線支付平臺
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項目第20戰(zhàn)之在線支付平臺,關(guān)于第三方支付的內(nèi)容從本文開始,感興趣的小伙伴們可以參考一下2016-06-06

