MyBatis 作為一款靈活的持久層框架,除了基礎的 CRUD 操作,還提供了連接池管理、動態 SQL 以及多表關聯查詢等高級特性。本文將從連接池原理出發,深入講解動態 SQL 的常用標簽,并通過實例演示一對多、多對多等復雜關聯查詢的實現,幫助你掌握 MyBatis 的進階用法。
一、MyBatis 連接池:提升數據庫交互性能
連接池是存儲數據庫連接的容器,它的核心作用是避免頻繁創建和關閉連接,從而減少資源消耗、提高程序響應速度。在 MyBatis 中,連接池的配置通過dataSource
標簽的type
屬性實現,支持三種類型的連接池:
1. 連接池類型詳解
POOLED:使用 MyBatis 內置的連接池
MyBatis 會維護一個連接池,當需要連接時從池中獲取,使用完畢后歸還給池,避免頻繁創建連接。適用于高并發場景,是開發中最常用的類型。配置示例:
<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>
UNPOOLED:不使用連接池
每次執行 SQL 時都會創建新的連接,使用后直接關閉。適用于低并發場景,性能較差,一般僅用于簡單測試。配置示例:
<dataSource type="UNPOOLED"><!-- 同POOLED的屬性配置 --> </dataSource>
JNDI:依賴容器的連接池
由 Web 容器(如 Tomcat)提供連接池管理,MyBatis 僅負責從容器中獲取連接。適用于Java EE 環境,需在容器中提前配置連接池。配置示例:
<dataSource type="JNDI"><property name="data_source" value="java:comp/env/jdbc/mybatis_db"/> </dataSource>
2. 連接池的優勢
- 資源復用:連接池中的連接可重復使用,減少創建連接的開銷;
- 響應速度:提前創建連接,避免 SQL 執行時的連接創建延遲;
- 并發控制:通過最大連接數限制,防止數據庫因連接過多而崩潰。
二、動態 SQL:靈活拼接 SQL 語句
在實際開發中,查詢條件往往是動態變化的(如多條件篩選、批量操作等)。MyBatis 的動態 SQL 標簽可以優雅地解決 SQL 語句拼接問題,避免手動拼接導致的語法錯誤和 SQL 注入風險。
1.?<if>
標簽:條件判斷
<if>
標簽用于根據參數值動態生成 SQL 片段,常用來處理多條件查詢。
示例場景:根據用戶名和性別查詢用戶(參數非空時才添加條件)。
UserMapper 接口:
public interface UserMapper {// 條件查詢用戶List<User> findByWhere(User user); }
UserMapper.xml 配置:
<select id="findByWhere" parameterType="user" resultType="user">select * from user<where><!-- 當username非空且非空字符串時,添加條件 --><if test="username != null and username != ''">and username like #{username}</if><!-- 當sex非空且非空字符串時,添加條件 --><if test="sex != null and sex != ''">and sex = #{sex}</if></where> </select>
測試代碼:
@Test public void testFindByWhere() {User user = new User();user.setUsername("%zz%"); // 模糊查詢包含"zz"的用戶名user.setSex("m");List<User> list = userMapper.findByWhere(user);// 遍歷結果... }
說明:test
屬性中的表達式用于判斷參數是否有效,where
標簽會自動處理多余的and
或or
,避免 SQL 語法錯誤。
?
2.?<foreach>
標簽:遍歷集合
<foreach>
標簽用于遍歷集合或數組,常用來處理in
查詢或批量操作。
場景 1:查詢 ID 在指定集合中的用戶(in
查詢)
User 實體類:添加存儲 ID 集合的屬性
public class User {private List<Integer> ids; // 存儲多個ID// 省略getter、setter }
UserMapper 接口:
List<User> findByIds(User user);
UserMapper.xml 配置:
<select id="findByIds" parameterType="user" resultType="user">select * from user<where><!-- collection:集合屬性名(此處為ids)open:SQL片段開頭close:SQL片段結尾separator:元素分隔符item:遍歷的元素別名--><foreach collection="ids" open="id in (" separator="," close=")" item="id">#{id}</foreach></where> </select>
測試代碼:
@Test public void testFindByIds() {User user = new User();List<Integer> ids = new ArrayList<>();ids.add(1);ids.add(2);ids.add(3);user.setIds(ids);List<User> list = userMapper.findByIds(user); // 查詢ID為1、2、3的用戶 }
?
場景 2:批量查詢(or
條件)
如需生成id = 1 or id = 2 or id = 3
形式的 SQL,只需調整<foreach>
的open
和separator
:
<foreach collection="ids" open="id = " separator="or id = " item="id">#{id}
</foreach>
3.?<sql>
與<include>
標簽:SQL 片段復用
對于頻繁使用的 SQL 片段(如查詢字段、表名等),可以用<sql>
標簽定義,再通過<include>
標簽引用,減少代碼冗余。
示例:復用查詢用戶的 SQL 片段。
- UserMapper.xml 配置:
<!-- 定義SQL片段 --> <sql id="userColumns">id, username, birthday, sex, address </sql><!-- 引用SQL片段 --> <select id="findAll" resultType="user">select <include refid="userColumns"/> from user </select>
說明:id
為片段唯一標識,refid
指定要引用的片段 ID,適用于多表查詢中重復的字段列表。
三、一對多查詢:用戶與賬戶的關聯
在實際業務中,表之間往往存在關聯關系(如用戶與賬戶:一個用戶可以有多個賬戶)。MyBatis 通過<collection>
標簽處理一對多關聯查詢。
1. 表結構與實體類設計
- 用戶表(user):存儲用戶基本信息(id、username 等);
- 賬戶表(account):存儲賬戶信息,通過
uid
關聯用戶表(多對一關系)。
實體類設計:
Account 類(多對一:一個賬戶屬于一個用戶):
public class Account implements Serializable {private Integer id;private Integer uid; // 關聯用戶IDprivate Double money;// 關聯的用戶對象private User user; // 省略getter、setter }
User 類(一對多:一個用戶有多個賬戶):
public class User implements Serializable {private Integer id;private String username;// 關聯的賬戶列表private List<Account> accounts; // 省略getter、setter }
2. 多對一查詢(賬戶關聯用戶)
查詢所有賬戶,并關聯查詢所屬用戶的信息。
AccountMapper 接口:
public interface AccountMapper {List<Account> findAll(); }
AccountMapper.xml 配置:
<select id="findAll" resultMap="accountMap"><!-- 關聯查詢賬戶和用戶 -->select a.*, u.username, u.address from account aleft join user u on a.uid = u.id </select><!-- 定義結果映射 --> <resultMap id="accountMap" type="account"><id property="id" column="id"/><result property="uid" column="uid"/><result property="money" column="money"/><!-- 關聯用戶對象(多對一) --><association property="user" javaType="user"><result property="username" column="username"/><result property="address" column="address"/></association> </resultMap>
說明:<association>
標簽用于映射關聯的單個對象,javaType
指定對象類型。
?
3. 一對多查詢(用戶關聯賬戶)
查詢所有用戶,并關聯查詢其名下的所有賬戶。
UserMapper 接口:
public interface UserMapper {// 查詢用戶及關聯的賬戶List<User> findOneToMany(); }
UserMapper.xml 配置:
<select id="findOneToMany" resultMap="userAccountMap">select u.*, a.id as aid, a.money from user uleft join account a on u.id = a.uid </select><resultMap id="userAccountMap" type="user"><id property="id" column="id"/><result property="username" column="username"/><result property="birthday" column="birthday"/><result property="sex" column="sex"/><result property="address" column="address"/><!-- 關聯賬戶列表(一對多) --><collection property="accounts" ofType="account"><id property="id" column="aid"/> <!-- 注意別名避免與用戶ID沖突 --><result property="money" column="money"/></collection> </resultMap>
說明:<collection>
標簽用于映射關聯的集合對象,ofType
指定集合中元素的類型。
?
四、多對多查詢:用戶與角色的關聯
多對多關系需要通過中間表實現(如用戶與角色:一個用戶可擁有多個角色,一個角色可分配給多個用戶,通過user_role
表關聯)。
1. 表結構與實體類設計
- 角色表(role):存儲角色信息(id、role_name 等);
- 中間表(user_role):通過
uid
和rid
關聯用戶表和角色表。
實體類設計:
- Role 類(多對多:一個角色包含多個用戶):
public class Role implements Serializable {private Integer id;private String role_name;private String role_desc;// 關聯的用戶列表private List<User> users;// 省略getter、setter }
2. 多對多查詢實現
查詢所有角色,并關聯查詢擁有該角色的用戶信息。
RoleDao 接口:
public interface RoleDao {List<Role> findAll(); }
RoleDao.xml 配置:
<select id="findAll" resultMap="roleMap">SELECT r.*, u.id as user_id, u.username FROM role rJOIN user_role ur ON r.id = ur.RIDJOIN user u ON u.id = ur.UID </select><resultMap type="role" id="roleMap"><id property="id" column="id"/><result property="role_name" column="role_name"/><result property="role_desc" column="role_desc"/><!-- 關聯用戶列表(多對多) --><collection property="users" ofType="user"><id property="id" column="user_id"/> <!-- 別名避免與角色ID沖突 --><result property="username" column="username"/></collection> </resultMap>
測試代碼:
@Test public void testFindAllRoles() {List<Role> roles = roleDao.findAll();for (Role role : roles) {System.out.println("角色:" + role.getRole_name());System.out.println("關聯用戶:" + role.getUsers());} }
說明:多對多查詢本質是雙向的一對多查詢,通過中間表建立關聯,同樣使用<collection>
標簽映射集合對象。
?
?
五、MyBatis 延遲加載策略
1. 延遲加載的概念
延遲加載(Lazy Loading)是一種數據庫查詢優化策略,其核心思想是:僅在需要使用關聯數據時才進行實際查詢。與立即加載(Eager Loading)相比,延遲加載避免了不必要的數據庫訪問,提高了系統性能。
對比示例(一對多關系):
- 立即加載:查詢用戶時,同時加載該用戶的所有賬戶信息(即使后續可能不使用賬戶數據)。
- 延遲加載:查詢用戶時,僅加載用戶基本信息;當程序調用
user.getAccounts()
時,才會觸發賬戶數據的查詢。
2. 應用場景選擇
場景 | 加載策略 | 示例 |
---|---|---|
多對一關系 | 立即加載 | 查詢賬戶時,同時加載所屬用戶 |
一對多 / 多對多 | 延遲加載 | 查詢用戶時,暫不加載賬戶信息 |
3. 多對一延遲加載實現
(1)配置文件示例
<!-- AccountMapper.xml -->
<resultMap type="Account" id="accountMap"><id column="id" property="id"/><result column="uid" property="uid"/><result column="money" property="money"/><!-- 配置延遲加載:通過select屬性指定關聯查詢方法 --><association property="user" javaType="User" select="com.qcbyjy.mapper.UserMapper.findById" column="uid"><id column="id" property="id"/><result column="username" property="username"/></association>
</resultMap>
(2)核心配置參數
<!-- SqlMapConfig.xml -->
<settings><!-- 開啟延遲加載功能 --><setting name="lazyLoadingEnabled" value="true"/><!-- 禁用積極加載(默認false,按需加載) --><setting name="aggressiveLazyLoading" value="false"/>
</settings>
?測試方法
@Testpublic void testFindAll1() throws IOException {// 先加載主配置文件,加載到輸入流中InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 創建SqlSessionFactory對象,創建SqlSession對象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 創建SqlSession對象SqlSession session = factory.openSession();// 獲取代理對象AccountMapper mapper = session.getMapper(AccountMapper.class);// 1. 查詢主對象(賬戶)List<Account> accounts = mapper.findAll();System.out.println("===== 主查詢已執行 =====");// 2. 遍歷賬戶,但不訪問關聯的用戶for (Account account : accounts) {System.out.println("賬戶ID:" + account.getId() + ",金額:" + account.getMoney());}System.out.println("===== 未訪問關聯對象 =====");// 3. 首次訪問關聯的用戶for (Account account : accounts) {System.out.println("===== 開始訪問用戶 =====");System.out.println("用戶名:" + account.getUser().getUsername()); // 觸發懶加載System.out.println("===== 訪問用戶結束 =====");}// 關閉資源session.close();inputStream.close();}
執行findAll()
時,日志僅輸出賬戶表的查詢 SQL?。遍歷賬戶但不訪問用戶時,無新的 SQL 輸出:
?
首次訪問account.getUser()
時,日志輸出用戶表的查詢 SQL(按需加載)
?
(3)工作原理
當執行account.getUser()
時,MyBatis 會:
- 檢查
lazyLoadingEnabled
是否為true
; - 通過
select
屬性調用UserMapper.findById(uid)
方法; - 將結果封裝到
Account.user
屬性中。
4. 一對多延遲加載實現
(1)配置文件示例
<!-- UserMapper.xml -->
<resultMap type="User" id="userMap"><id column="id" property="id"/><result column="username" property="username"/><!-- 配置延遲加載:集合屬性 --><collection property="accounts" ofType="Account" select="com.qcbyjy.mapper.AccountMapper.findByUid" column="id"><id column="id" property="id"/><result column="money" property="money"/></collection>
</resultMap>
(2)延遲加載觸發時機
List<User> users = userMapper.findAll();
for (User user : users) {// 調用getAccounts()時觸發延遲查詢System.out.println(user.getAccounts());
}
測試代碼:
@Testpublic void testFindAllq() throws Exception {// 調用方法List<User> list = mapper.findAll();for (User user : list) {System.out.println(user.getUsername());System.out.println(user.getAccounts());System.out.println("==============");}}
?
5. 延遲加載注意事項
- N+1 查詢問題:延遲加載可能導致 N+1 查詢(主查詢 1 次,關聯查詢 N 次),需結合二級緩存優化。
- Session 生命周期:延遲加載需確保關聯查詢時
SqlSession
未關閉(可通過openSession(true)
保持會話)。 - 序列化問題:延遲加載的對象在序列化時可能丟失代理狀態,需通過
<setting name="serializationFactory" value="..."/>
配置。
七、MyBatis 緩存機制
1. 緩存的基本概念
緩存是一種內存臨時存儲技術,用于減少數據庫訪問次數,提高系統響應速度。適合緩存的數據特點:
- 頻繁查詢但很少修改;
- 數據一致性要求不高;
- 數據量適中且訪問頻率高。
2. 一級緩存(SqlSession 級緩存)
(1)緩存原理
- 作用域:每個
SqlSession
獨享一個緩存實例; - 存儲結構:底層使用
PerpetualCache
(基于HashMap
實現); - 生命周期:與
SqlSession
一致,session.close()
后緩存銷毀。
(2)緩存驗證示例
@Test
public void testFirstLevelCache() {try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);// 第一次查詢:觸發SQLUser user1 = mapper.findById(1);// 第二次查詢:命中緩存User user2 = mapper.findById(1);System.out.println(user1 == user2); // 輸出true(同一對象)}
}
?
(3)緩存失效場景
以下操作會導致一級緩存清空:
session.clearCache()
:手動清空緩存;session.commit()
/session.rollback()
:事務提交或回滾;- 執行
insert
/update
/delete
操作(任何數據變更)。
3. 一級緩存源碼分析
核心源碼位于BaseExecutor
類:
// BaseExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);// 1. 創建緩存KeyCacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);// 2. 查詢一級緩存return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 從本地緩存中獲取List<E> list = localCache.getObject(key);if (list != null) {// 緩存命中handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);return list;} else {// 緩存未命中,查詢數據庫return queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}
}
4. 一級緩存的應用建議
- 優勢:無需額外配置,自動生效,適用于單次會話內的重復查詢;
- 局限:無法跨
SqlSession
共享,對長事務可能導致數據不一致; - 最佳實踐:
- 避免在同一
SqlSession
內進行重復查詢; - 及時提交事務或關閉
SqlSession
以釋放緩存資源。
- 避免在同一
八、延遲加載與一級緩存的協同工作
當延遲加載與一級緩存結合時,需注意:
- 關聯查詢緩存:延遲加載的關聯對象(如
user.getAccounts()
)會被存入一級緩存; - 會話隔離:不同
SqlSession
的延遲加載結果相互獨立; - 數據一致性:若主對象已緩存,關聯對象的變更可能無法實時反映。
mysql緩存存的是語句,稍有修改就會更新緩存。一級緩存,輸出的對象是同一個,二級緩存輸出不同是因為通過序列化組裝了兩
一、一級緩存(SqlSession 級緩存)—— 同一個對象實例
1. 緩存本質與作用范圍
一級緩存是?SqlSession 私有?的本地緩存,MyBatis 默認開啟。在同一個 SqlSession 內,只要查詢條件、SQL 語句相同,MyBatis 會直接從緩存取結果,不會重復訪問數據庫。
2. “輸出對象是同一個” 的原因
- 當執行查詢時,MyBatis 會先檢查一級緩存:若命中緩存,直接返回?緩存中存儲的對象引用?。
- 也就是說,多次查詢拿到的是同一個 Java 對象實例(JVM 中同一個內存地址的對象 )。例如:
try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);// 第一次查詢,從數據庫加載,存入一級緩存User user1 = mapper.getUserById(1); // 第二次查詢,命中一級緩存,直接返回 user1 的引用User user2 = mapper.getUserById(1); System.out.println(user1 == user2); // 輸出 true,是同一個對象實例
}
?
3. 緩存失效場景
當執行?update
、insert
、delete
、commit
、close
?等操作時,一級緩存會被清空 。后續查詢會重新從數據庫加載數據,存入新的對象實例到緩存。
二、二級緩存(Mapper 級緩存)—— 不同對象實例(因序列化 / 反序列化)
1. 緩存本質與作用范圍
二級緩存是?Mapper 作用域?的緩存,需手動開啟(在 Mapper XML 或注解中配置 )。它可以在多個 SqlSession 間共享,底層通常依賴序列化 / 反序列化機制存儲數據 。
2. “輸出不同對象” 的原因
- 二級緩存存儲的是?對象的序列化數據?(如 Java 對象先序列化為字節流,再存入緩存 )。
- 當不同 SqlSession 查詢命中二級緩存時,MyBatis 會?反序列化?緩存中的字節流,重新生成一個新的 Java 對象實例 。例如:
// 開啟二級緩存后,不同 SqlSession 測試
try (SqlSession session1 = sqlSessionFactory.openSession()) {UserMapper mapper1 = session1.getMapper(UserMapper.class);User user1 = mapper1.getUserById(1); session1.commit(); // 提交后,數據可能同步到二級緩存(取決于配置)
}try (SqlSession session2 = sqlSessionFactory.openSession()) {UserMapper mapper2 = session2.getMapper(UserMapper.class);User user2 = mapper2.getUserById(1); // 命中二級緩存,反序列化生成新對象System.out.println(user1 == user2); // 輸出 false,是不同對象實例
}
?
3. 二級緩存的核心特點
- 跨 SqlSession 共享:多個 SqlSession 可共用 Mapper 級的緩存數據;
- 序列化存儲:緩存數據需實現?
Serializable
?接口,存儲和讀取時會經歷序列化 / 反序列化,因此每次命中緩存會生成新對象; - 緩存策略靈活:可配置?
eviction
(回收策略,如 LRU、FIFO )、flushInterval
(刷新間隔 )、readOnly
(是否只讀 )等。
三、一、二級緩存的核心差異對比
對比項 | 一級緩存 | 二級緩存 |
---|---|---|
作用范圍 | SqlSession 私有 | Mapper 作用域(跨 SqlSession 共享) |
對象實例 | 同一對象引用 | 反序列化生成新對象 |
開啟方式 | 默認開啟 | 需手動配置(<cache> ?標簽或注解) |
存儲機制 | 直接存對象引用 | 存序列化后的字節流 |
數據一致性 | 依賴 SqlSession 內操作,易維護 | 需注意多表關聯、更新同步問題 |
四、實際開發中的注意事項
一級緩存的 “隱式風險”:
若在同一個 SqlSession 內,先查詢再更新數據,由于一級緩存未及時清理(需手動 commit/close 觸發 ),可能拿到舊數據。建議在增刪改后,及時 commit 或 close SqlSession,保證緩存與數據庫一致。二級緩存的 “使用前提”:
啟用二級緩存時,實體類必須實現?Serializable
?接口(否則序列化報錯 );同時,若涉及多表關聯查詢,需注意緩存的更新策略(比如某張表數據變化后,關聯的 Mapper 緩存需及時刷新 )。緩存的合理選擇:
- 一級緩存適合短生命周期的 SqlSession(如單次請求內的多次查詢 );
- 二級緩存適合查詢頻率高、數據變化少的場景(如系統字典表 ),但需謹慎處理數據更新后的緩存同步。
簡單來說,一級緩存是 “同一個對象復用”,二級緩存是 “序列化后重新組裝對象”,這種差異由它們的作用范圍和存儲機制決定。開發中根據業務場景合理利用緩存,既能提升性能,又能避免數據一致性問題~ 若你在實際配置或調試緩存時遇到具體問題(比如二級緩存不生效、序列化報錯 ),可以接著展開說場景幫你分析 。
?