最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MyBatis進(jìn)階教程之動態(tài)SQL、關(guān)聯(lián)查詢與緩存

 更新時間:2025年11月11日 10:31:53   作者:小馬不敲代碼  
在Java持久層技術(shù)體系中,MyBatis憑借其靈活的SQL映射和強大的動態(tài)SQL能力,成為企業(yè)級應(yīng)用開發(fā)的首選框架,這篇文章主要介紹了MyBatis進(jìn)階教程之動態(tài)SQL、關(guān)聯(lián)查詢與緩存的相關(guā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中

    本文文章給大家介紹如何將一個JAR包添加到Java應(yīng)用程序的Boot?Classpath中,本文通過實例代碼給大家介紹的非常詳細(xì),需要的的朋友參考下吧
    2023-11-11
  • Springboot es包版本異常解決方案

    Springboot es包版本異常解決方案

    這篇文章主要介紹了springboot 項目依賴 es包版本異常,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-03-03
  • PropertiesLoaderUtils 出現(xiàn)中文亂碼的解決方式

    PropertiesLoaderUtils 出現(xiàn)中文亂碼的解決方式

    這篇文章主要介紹了PropertiesLoaderUtils 出現(xiàn)中文亂碼的解決方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-08-08
  • Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀

    Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀

    這篇文章主要介紹了Java線程池ThreadPoolExecutor的使用及其原理詳細(xì)解讀,線程池是一種多線程處理形式,處理過程中將任務(wù)添加到隊列,然后在創(chuàng)建線程后自動啟動這些任務(wù),線程池線程都是后臺線程,需要的朋友可以參考下
    2023-12-12
  • try-with-resource優(yōu)雅關(guān)閉io流的方法

    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)詳解

    JavaWeb Struts文件上傳功能實現(xiàn)詳解

    這篇文章主要為大家詳細(xì)介紹了JavaWeb Struts文件上傳功能實現(xiàn)過程,思路清晰,供大家參考,感興趣的小伙伴們可以參考一下
    2016-06-06
  • Spring?MVC內(nèi)置過濾器功能示例詳解

    Spring?MVC內(nèi)置過濾器功能示例詳解

    這篇文章主要為大家介紹了Spring?MVC內(nèi)置過濾器使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Spark集群框架的搭建與入門

    Spark集群框架的搭建與入門

    Spark是專為大規(guī)模數(shù)據(jù)處理而設(shè)計的,基于內(nèi)存快速通用,可擴展的集群計算引擎,實現(xiàn)了高效的DAG執(zhí)行引擎,可以通過基于內(nèi)存來高效處理數(shù)據(jù)流,運算速度相比于MapReduce得到了顯著的提高。
    2021-06-06
  • Java中的對象和引用詳解

    Java中的對象和引用詳解

    這篇文章主要介紹了Java中的對象和引用詳解的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • SSH框架網(wǎng)上商城項目第20戰(zhàn)之在線支付平臺

    SSH框架網(wǎng)上商城項目第20戰(zhàn)之在線支付平臺

    這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項目第20戰(zhàn)之在線支付平臺,關(guān)于第三方支付的內(nèi)容從本文開始,感興趣的小伙伴們可以參考一下
    2016-06-06

最新評論

高碑店市| 东乌珠穆沁旗| 奉化市| 韶关市| 莲花县| 霍州市| 灯塔市| 旺苍县| 临西县| 扶绥县| 乐东| 榕江县| 绍兴县| 淮安市| 东辽县| 金阳县| 木兰县| 理塘县| 漳浦县| 商都县| 昭觉县| 北流市| 台南县| 滨州市| 南岸区| 溆浦县| 商水县| 昆明市| 循化| 葫芦岛市| 石家庄市| 万荣县| 黑龙江省| 松桃| 昌都县| 靖安县| 洛宁县| 沙洋县| 呼玛县| 张北县| 邵武市|