1. 程式人生 > >Mybatis詳解系列(一)--持久層框架解決了什麼及如何使用Mybatis

Mybatis詳解系列(一)--持久層框架解決了什麼及如何使用Mybatis

# 簡介 Mybatis 是一個持久層框架,它對 JDBC 進行了高階封裝,使我們的程式碼中不會出現任何的 JDBC 程式碼,另外,它還通過 xml 或註解的方式將 sql 從 DAO/Repository 層中解耦出來,除了這些基本功能外,它還提供了動態 sql、延遲載入、快取等功能。 相比 Hibernate,Mybatis 更面向資料庫,可以靈活地對 sql 語句進行優化。 針對 Mybatis 的分析,我會拆分成**使用、配置、原始碼、生成器**等部分,都放在 [Mybatis]( https://www.cnblogs.com/ZhangZiSheng001/category/1685176.html ) 這個系列裡,內容將持續更新。本文是這個系列的第一篇文章,將從以下兩個問題展開 : 1. 持久層框架解決了哪些問題? 2. 如何使用 Mybatis(這裡會從入門到深入)? # 專案環境的說明 為了更好地分析 Mybatis 的特性,本專案不會引入任何的依賴注入框架,將使用比較原生態的方式來使用 Mybatis。 ## 工程環境 JDK:1.8.0_231 maven:3.6.1 IDE:Spring Tool Suites4 for Eclipse 4.12 (裝有 Mybatipse 外掛) mysql:5.7.28 ## 依賴引入 Mybatis 有自帶的連線池,但實際專案中建議還是引入第三方的比較好。 ```xml org.Mybatis Mybatis 3.5.4 mysql mysql-connector-java 8.0.15 ch.qos.logback logback-core 1.2.3 jar ch.qos.logback logback-classic 1.2.3 jar com.zaxxer HikariCP 2.6.1 junit
junit 4.12 test
``` ## 資料庫指令碼 在這個專案裡面,我希望儘可能地模擬出實際專案的各種場景,例如,高階條件查詢、關聯查詢(一對一關聯、多對多關聯和自關聯),並研究在對應場景下如何使用 Mybatis 解決問題。本專案的 ER 圖如下,涉及到 4 張主表和 2 張中間表,具體的[sql指令碼](https://github.com/ZhangZiSheng001/mybatis-projects/sql)也提供好了: ![Mybatis_demo專案的ER圖](https://img2020.cnblogs.com/blog/1731892/202003/1731892-20200331111457093-813152020.png) # 持久層框架解決了哪些問題 在分析如何使用 mybatis 之前,我們先來研究一個問題:持久層框架解決了哪些問題? 假設沒有持久層框架,首先想到的就是使用 JDBC 來操作資料庫。這裡我簡單地引入一個需求,就是我想通過 id 查詢出一個員工物件。下面僅會從repository/DAO 層的角度來考慮如何實現,所以我們不需要去考慮 service 層中**事務提交**和**連線關閉**的問題,當然,這樣會遇到一個問題,就是我們必須保證 service 層的事務和持久層的是同一個,這一點會通過 **Utils 來解決,因為不是本文重點,這裡不展開。 ## 用 JDBC 方式查詢一個員工 下面用 JDBC 查詢員工物件。 有人可能會問,就算你不適用持久層框架,你還可以使用 DBUtils 或者自己封裝 JDBC 程式碼啊?這裡需要強調下,這種封裝其實是持久層框架應該做的事,我們自己手動封裝,其實已經在實現一個持久層框架了。所以,為了暴露純粹的 JDBC 實現的缺點,這裡儘量不去封裝。 ```java @Override public Employee selectByPrimaryKey(String id) throws SQLException { Employee employee = null; PreparedStatement statement = null; ResultSet resultSet = null; // 建立sql String sql = "select * from demo_employee where id = ?"; try { // 獲得連線(JDBCUtils保證同一執行緒獲得同一個連線物件) Connection connection = JDBCUtils.getConnection(); // 獲得Statement物件 statement = connection.prepareStatement(sql); // 設定引數 statement.setObject(1, id); // 執行,獲取結果集 resultSet = statement.executeQuery(); if(resultSet.next()) { // 對映結果集 employee = convert(resultSet); } // 返回員工物件 return employee; } finally { // 釋放資源 JDBCUtils.release(null, statement, resultSet); } } /** *

通過結果集構造員工物件

* @author: zzs * @date: 2020年3月28日 下午12:20:02 * @param resultSet * @return: Employee * @throws SQLException */ private Employee convert(ResultSet resultSet) throws SQLException { Employee employee = new Employee(); employee.setId(resultSet.getString("id")); employee.setName(resultSet.getString("name")); employee.setGender(resultSet.getBoolean("gender")); employee.setNo(resultSet.getString("no")); employee.setAddress(resultSet.getString("address")); employee.setDeleted(resultSet.getBoolean("deleted")); employee.setDepartmentId(resultSet.getString("department_id")); employee.setPassword(resultSet.getString("password")); employee.setPhone(resultSet.getString("phone")); employee.setStatus(resultSet.getByte("status")); employee.setCreate(resultSet.getDate("gmt_create")); employee.setModified(resultSet.getDate("gmt_modified")); return employee; } ``` 通過上面的程式碼,我們可以看到兩個主要的問題: 1. **每個 Repository/DAO 方法都會出現繁瑣、重複的 JDBC 程式碼**。 2. **sql 和 DAO/Repository 的程式程式碼耦合度太高,不能統一管理**。這裡的 sql 包括了 sql 的定義、引數設定和結果集對映,強調一點,**不是說 sql 不能出現在 java 類中,而是說應該從 DAO/Repository 的程式程式碼中解耦出來,進行集中管理**。 說到這裡,我們可以總結出來,為了專案的方便和解耦,一個基本的持久層框架需要做到: 1. **對 JDBC 程式碼進行高階封裝,為我們提供更簡單的介面**。 2. **將 sql 從 DAO/Repository 中解耦出來**。 Mybatis 作為一個優秀的持久層框架,針對以上問題提供瞭解決方案,下面我們再看看使用 Mybatis 如何實現上面的需求。 ## 用 Mybatis 方式查詢一個員工 還是通過查詢員工的例子來說明,程式碼如下: ```java @Override public Employee selectByPrimaryKey(String id) { // 獲取sqlSession SqlSession sqlSession = MybatisUtils.getSqlSession(); // 獲取Mapper EmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class); // 執行,獲取員工物件 Employee employee = baseMapper.selectByPrimaryKey(id); // 返回物件 return employee; } ``` 上面的程式碼沒有出現任何的 JDBC 程式碼和 sql 程式碼,因為 Mybatis 對 JDBC 進行了高階封裝,並且採用 Mapper 的註解或 xml 檔案來統一管理 sql 的定義、引數設定和結果集對映。下面看下 xml 檔案的方式: ```xml
e.id, e.`name`, e.gender, e.no, e.password, e.phone, e.address, e.status, e.deleted, e.department_id, e.gmt_create, e.gmt_modified ``` 針對 sql 解耦的問題,早期的持久層框架都偏向於將 sql 獨立在配置檔案中,後來才逐漸引入註解的支援,如下是Mybatis 的註解方式(EmployeeMapper 介面): ```java @Select("SELECT e.id, e.`name`, e.gender, e.no, e.password, e.phone, e.address, e.status, e.deleted, e.department_id, e.gmt_create, e.gmt_modified FROM demo_employee e WHERE id = #{id}") @resultMap("BaseResultMap") Employee selectByPrimaryKey(String id); ``` 我認為,正如前面說到的,sql 在專案中存在形式不是重點,我們的目的是希望 sql 能被統一管理,基於這個目的實現的不同方案,都是合理的。 Mybatis 作為一款優秀的持久層框架,除了解決上面的兩個基本問題,還為我們提供了懶載入、快取、動態語句、外掛等功能,下文會講到。 ## 補充 通過上面的內容,我們已經回答了問題:持久層框架解決了哪些問題?這裡需要補充一點: 本文只是指出持久層框架的需要解決的基本問題,並沒有強調必須使用 Mybatis 或 Hibernate 等通用框架。出於效能方面的考慮,部分開發者可能會採用更輕量的實現,而不是使用流行的通用框架。當然,這也是自己造輪子和使用通用輪子的區別了。 # 如何使用 Mybatis 本專案會模擬實際開發的各種場景來研究 Mybatis 的使用方法。在我看來,在專案建立時,repository/DAO 層只要有以下幾個方法,已經可以滿足大部分使用需求。**在 repository/DAO 層定義大量的`*By*`方法是非常低階和不負責任的**,然而,我接觸過許多人都是這麼搞的。 ```java public interface IEmployeeRepository { // 查詢 Employee get(String id);//根據id查詢 List list(EmployeeCondition con);//根據條件查詢 long count(EmployeeCondition con);//根據條件查詢數量 // 刪除 int delete(EmployeeCondition con);//根據條件刪除 int delete(String id);//根據id刪除 // 新增 int save(Employee employee);//新增 int save(List list);//批量新增 // 更新 int update(Employee employee, EmployeeCondition con);//根據條件更新 int update(Employee employee);//更新 } ``` 下面的使用例子將針對這個介面進行展開,主要分成4個部分: 1. 入門例子。通過 根據id查詢員工和新增員工 的例子說明; 2. 高階條件查詢。 3. 關聯查詢。這裡會查詢員工並帶出部門、角色,並且結合懶載入使用。 ## 從入門例子開始 本文的包結構如下。test 裡的測試簡單看成是 service 層在呼叫 respository 層的方法,由於我必須在 service 層 和 respository 層中拿到同一個“連線”來管理事務或釋放資源,所有 util 中將“連線”繫結到了當前執行緒。 在進行下面工作之前,我們需要先建立好實體和 mapper 檔案(如圖圈紅部分),實際專案中,我們可以使用 [Mybatis-generator]() 或者自定義的程式碼生成器生成,mapper 中將包含基本的 CRUD 程式碼。 ### 配置 configuration 檔案 這個是 Mybatis 的主配置檔案,它**影響著 Mybatis 的行為和屬性資訊**。配置檔案的層級結構如下: configuration(配置) - properties(屬性) - settings(設定) - typeAliases(類型別名) - typeHandlers(型別處理器) - objectFactory(物件工廠) - plugins(外掛) - environments(環境配置) - environment(環境變數) - transactionManager(事務管理器) - dataSource(資料來源) - databaseIdProvider(資料庫廠商標識) - mappers(對映器) 作為入門例子,這裡只進行了簡單的配置,本系列的第二篇部落格將詳細講解這些配置。 注意:**configuration 的標籤必須按順序寫,不然會報錯。** ```xml ``` 以上配置作用如下: 1. **typeAliases**:類型別名,僅在 *Mapper.xml 中使用。通過配置實體類的包名,我們可以在 xml 中直接通過 Employee 來表示員工型別,而不需要使用全限定類名; 2. **environments**:環境配置。下篇文章再詳細講吧。這裡簡單說下 dataSource,由於引入的是第三方資料來源,所以得重寫 org.apache.ibatis.datasource.DataSourceFactory介面,如下: ```java public class HikariDataSourceFactory implements DataSourceFactory { private DataSource dataSource; public HikariDataSourceFactory() { super(); try { HikariConfig config = new HikariConfig("/hikari.properties"); dataSource = new HikariDataSource(config); } catch(Exception e) { throw new RuntimeException("建立資料來源失敗", e); } } @Override public DataSource getDataSource() { return dataSource; } @Override public void setProperties(Properties props) { // TODO Auto-generated method stub } } ``` 3. **mappers**:對映器。其實就是告訴 Mybatis 對映器放在哪裡,注意,Mapper 介面和 xml 檔案**編譯打包後**必須在同一個路徑下。如果你的 xml 檔案放在 src/main/java 中(不建議),需要在 pom 檔案中增加以下配置: ```xml src/main/java false **/mapper/*.xml src/main/resources ** ``` ### 配置 mapper xml檔案 Mybatis 的對映檔案只有很少的幾個頂級元素(按照應被定義的順序列出): - `cache` – 該名稱空間的快取配置。 - `cache-ref` – 引用其它名稱空間的快取配置。 - `resultMap` – 描述如何從資料庫結果集中載入物件,是最複雜也是最強大的元素。 - `parameterMap` – 老式風格的引數對映。此元素已被廢棄,並可能在將來被移除!請使用行內參數對映。文件中不會介紹此元素。 - `sql` – 可被其它語句引用的可重用語句塊。 - `insert` – 對映插入語句。 - `update` – 對映更新語句。 - `delete` – 對映刪除語句。 - `select` – 對映查詢語句。 這裡也只進行了簡單的配置,本系列的第二篇部落格再詳細講。 ```xml e.id, e.`name`, e.gender, e.no, e.password, e.phone, e.address, e.status, e.deleted, e.department_id, e.gmt_create, e.gmt_modified insert into demo_employee (id, name, gender,no, password, phone,address, status, deleted,department_id, gmt_create, gmt_modified) values ( #{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{gender,jdbcType=BIT}, #{no,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR}, #{address,jdbcType=VARCHAR}, #{status,jdbcType=TINYINT}, #{deleted,jdbcType=BIT}, #{departmentId,jdbcType=VARCHAR}, #{create,jdbcType=TIMESTAMP}, #{modified,jdbcType=TIMESTAMP} ) ``` 在以上配置中,使用了三個元素: 1. **resultMap**:表列名(或查詢出來的別名)與實體屬性的對映關係。除了 id 和關聯物件欄位外,只要表列名(或查詢出來的別名)與實體屬性一致,可以不用配置。 2. **sql**: 用來定義可重用的 SQL 程式碼片段,可以在查詢或變更語句中通過 include 引用。如果資料庫的欄位名和實體類的不一致,需要設定列別名。 3. **select**: 查詢語句。其中,id 是所在名稱空間中唯一的識別符號,可以被用來引用這條語句,與 mapper 檔案中的,parameterType 是入參型別,resultMap 是對映表。 4. **insert**:插入語句。 注意下引數符號 #{id }, 它告訴 Mybatis 建立一個預處理語句(PreparedStatement)引數,在 JDBC 中,這樣的一個引數在 SQL 中會由一個“?”來標識,並被傳遞到一個新的預處理語句中。不過有時你就是想直接在 SQL 語句中直接插入一個不轉義的字串。 比如 ORDER BY 子句,這時候你可以使用 “$” 字元: ```xml ORDER BY ${columnName} ``` ### 獲取 SqlSession 在以下程式碼中,存在三個主要物件: 1. `SqlSessionFactoryBuilder` :一旦建立了 SqlSessionFactory,就不再需要它了,因此 SqlSessionFactoryBuilder 例項的最佳作用域是方法作用域。 2. `SqlSessionFactory`:一旦被建立就應該在應用的執行期間一直存在,沒有任何理由丟棄它或重新建立另一個例項, 因此 SqlSessionFactory 的最佳作用域是應用作用域。 3. `SqlSession`:每個執行緒都應該有它自己的 SqlSession 例項。SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。 **SqlSession 的作用類似於 JDBC 的`Connection`**,使用完後必須 close。 ```java // 載入配置檔案,初始化SqlSessionFactory物件 String resource = "Mybatis-config.xml"; InputStream in = Resources.getResourceAsStream(resource)); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in); // 獲取sqlSession 注意,這種方式獲取的 SqlSession 需手動提交事務。 SqlSession sqlSession = sqlSessionFactory.openSession(); ``` 為了保證同一個執行緒在 service 和 repository 中拿到同一個 SqlSession 物件,本專案中定義了工具類 cn.zzs.Mybatis.util.MybatisUtils 來獲取 SqlSession。 ```java public class MybatisUtils { private static SqlSessionFactory sqlSessionFactory; private static ThreadLocal tl = new ThreadLocal<>(); private static final Object obj = new Object(); static { init(); } /** * *

獲取SqlSession物件的方法,執行緒安全

* @author: zzs * @date: 2019年8月31日 下午9:22:29 * @return: SqlSession */ public static SqlSession getSqlSession() { // 從當前執行緒中獲取連線物件 SqlSession sqlSession = tl.get(); // 判斷為空的話,建立連線並繫結到當前執行緒 if(sqlSession == null) { synchronized(obj) { if((sqlSession = tl.get()) == null) { sqlSession = sqlSessionFactory.openSession(); tl.set(sqlSession); } } } return sqlSession; } /** *

根據指定配置檔案初始化SqlSessionFactory物件

* @author: zzs * @date: 2019年9月1日 上午10:53:05 * @return: void */ private static void init() { try (InputStream inputStream = Resources.getResourceAsStream("Mybatis-config.xml")) { // 載入配置檔案,初始化SqlSessionFactory物件 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch(IOException e) { throw new RuntimeException("建立sqlSessionFactory失敗", e); } } } ``` ### 編寫 Repository repository 的程式碼非常簡單,只需要拿到 SqlSessionn 物件,就能直接進行資料庫操作了。注意,**這裡的 SqlSession 不能作為例項變數**。 ```java public class EmployeeRepository implements IEmployeeRepository { @Override public Employee get(String id) { return MybatisUtils.getSqlSession().getMapper(EmployeeMapper.class).selectByPrimaryKey(id); } @Override public int save(Employee employee) { return MybatisUtils.getSqlSession().getMapper(EmployeeMapper.class).insert(employee); } } ``` ### 編寫測試類 測試類簡單看成是一個 service 類(當然,它不完全是),這裡需要手動地提交事務和釋放資源。 ```java public class EmployeeRepositoryTest { private IEmployeeRepository employeeRepository = new EmployeeRepository(); @Test public void testGet() { String id = "cc6b08506cdb11ea802000fffc35d9fe"; try (SqlSession sqlSession = MybatisUtils.getSqlSession();) { // 執行,獲取員工物件 Employee employee = employeeRepository.get(id); // 列印 System.out.println(employee); } } } @Test public void testSave() { // 建立使用者 Employee employee = new Employee(UUID.randomUUID().toString().replace("-", ""), "zzs005", true, "zzs005", "admin", "18826****41", "廣東", (byte)1, false, "94e2d2e56cd811ea802000fffc35d9fa", new Date(), new Date()); try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { // 儲存 employeeRepository.save(employee); // 提交事務 sqlSession.commit(); } } ``` ### 測試 測試上面兩個方法,會在控制檯輸出了 sql。為了直觀點,我這裡格式化了一下。 ```sql 2020-03-30 20:40:11.098 c.z.m.mapper.EmployeeMapper.selectByPrimaryKey - ==> Preparing: SELECT e.id, e.`name`, e.gender, e.no, e.password , e.phone, e.address, e.status, e.deleted, e.department_id , e.gmt_create, e.gmt_modified FROM demo_employee e WHERE e.id = ? 2020-03-30 20:40:11.121 c.z.m.mapper.EmployeeMapper.selectByPrimaryKey - ==> Parameters: cc6b08506cdb11ea802000fffc35d9fe(String) 2020-03-30 20:40:11.170 c.z.m.mapper.EmployeeMapper.selectByPrimaryKey - <== Total: 1 Employee [id=cc6b08506cdb11ea802000fffc35d9fe, name=zzf001, gender=false, no=zzf001, password=123456, phone=18826****41, address=北京, status=1, deleted=false, departmentId=65684a126cd811ea802000fffc35d9fa, create=Wed Sep 04 21:54:49 CST 2019, modified=Wed Sep 04 21:54:51 CST 2019] 2020-03-30 20:40:48.872 cn.zzs.Mybatis.mapper.EmployeeMapper.insert - ==> Preparing: INSERT INTO demo_employee (id, name, gender, no, password , phone, address, status, deleted, department_id , gmt_create, gmt_modified) VALUES (?, ?, ?, ?, ? , ?, ?, ?, ?, ? , ?, ?) 2020-03-30 20:40:48.899 cn.zzs.Mybatis.mapper.EmployeeMapper.insert - ==> Parameters: 517cabff75b24129b54048ce7d3280f9(String), zzs005(String), true(Boolean), zzs005(String), admin(String), 18826****41(String), 廣東(String), 1(Byte), false(Boolean), 94e2d2e56cd811ea802000fffc35d9fa(String), 2020-03-30 20:40:47.808(Timestamp), 2020-03-30 20:40:47.808(Timestamp) 2020-03-30 20:40:48.991 cn.zzs.Mybatis.mapper.EmployeeMapper.insert - <== Updates: 1 ``` ### 補充--批量新增 這裡我再補充下批量新增的實現。只要在上面 insert 語句的基礎上增加一個 foreach 標籤就可以,非常方便。在本系列第二篇文章中我將說到這些動態 sql 的用法。 ```xml insert into demo_employee (id, name, gender,no, password, phone,address, status, deleted,department_id, gmt_create, gmt_modified) values ( #{item.id}, #{item.name}, #{item.gender}, #{item.no}, #{item.password}, #{item.phone}, #{item.address}, #{item.status}, #{item.deleted}, #{item.departmentId}, #{item.create}, #{item.modified} ) ``` ## 高階條件查詢 還是回到下面這個介面,經過上面的例子,圈紅的幾個方法,相信大家已經知道如何使用。現在看看高階條件查詢。 ### 條件類和它的繼承體系 在專案中,條件類經常會被用來接收各種查詢條件,當業務比較複雜時,條件類會非常臃腫,大部分原因都是寫程式碼不遵循規範。我們的條件封裝類的條件由三個部分組成(以員工條件類為例): 1. 不同實體都會用到的條件,例如頁碼頁數; 2. 對應實體的屬性,例如員工性別、電話號碼; 3. 與對應實體關聯的實體屬性,例如員工所在部門名。 根據這種結構可以形成以下的繼承結構: 其中,`BaseCondition`中用於定義一些不同實體都通用的條件,如下: ```java public class BaseCondition { /** * 頁碼 */ private Integer pageNum; /** * 每頁記錄數 */ private Integer pageSize; /** * 排序語句 */ private String orderByClause; /** * 關鍵字 */ private String searchKeyWord; /** * 是否去重 */ private boolean distinct; // 省略setter/getter方法 } ``` AbstractEmployeeCondition 中定義屬於員工類的條件,如下: ```java public abstract class AbstractEmployeeCondition extends BaseCondition { /** * 注意,這裡不要命名為id */ private String employeeId; private String name; private Boolean gender; private String no; private String password; private String phone; private String address; private Byte status; private Boolean deleted; private String departmentId; private Date createStart; private Date createEnd; private Date modifiedStart; private Date modifiedEnd; // 省略setter/getter方法 } ``` 接下來是具體實現類 EmployeeCondition,這裡用於定義一些關聯實體的條件,也就是說使用到這些條件時必須 join 表。 ```java public class EmployeeCondition extends AbstractEmployeeCondition { //============部門表============ /** *

部門編號

*/ private String departmentNo; /** *

部門名

*/ private String departmentName; public boolean isJoinDepartment() { return (departmentNo != null && !departmentNo.isEmpty()) || (departmentName != null && !departmentName.isEmpty()); } // 省略setter/getter方法 } ``` ### 編寫 mapper xml檔案 Mybatis 提供了豐富的動態 sql 語法,以下可以完成高階條件查詢的 sql 拼接。 ```java and e.name = #{con.name} and e.gender = #{con.gender} and e.no = #{con.no} and e.password = #{con.password} and e.phone = #{con.phone} and e.address = #{con.address} and e.status = #{con.status} and e.deleted = #{con.deleted} and e.gmt_create > #{con.createStart} and e.gmt_create < #{con.createEnd} and e.gmt_modified > #{con.modifiedStart} and e.gmt_modified < #{con.modifiedEnd} and d.no = #{con.departmentNo} and d.name = #{con.departmentName} inner join demo_department d ``` 這裡的 sql 將條件分離出來複用,並沿用了條件實體的繼承關係,有利於後續專案維護和擴充套件。注意,千萬不要等到專案很臃腫時再進行 sql 的抽取複用。 上面高階條件查詢的程式碼,實際專案中可以通過程式碼生成器生成,擴充套件的條件再手動新增就行了。 ### 編寫測試方法 其實,這裡存在一個問題,排序條件那裡 sql 語句滲透到了 service 層,實際專案中,排序規則不會經常變動,我們可以在 xml 裡直接使用預設排序條件,條件類增加 userDefaultSort 屬性來判斷。總之要記住一點,在 service 層中滲透 sql 程式碼,是非常不應該的! ```java @Test public void testList() { EmployeeCondition con = new EmployeeCondition(); // 設定條件 con.setGender(false); con.setAddress("北京"); con.setDeleted(false); con.setPhone("18826****41"); con.setDistinct(true); con.setDepartmentNo("202003230002"); // 設定排序規則 con.setOrderByClause("name desc");// 注意為資料庫欄位 try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { // 執行 List list = employeeRepository.list(con); // 遍歷結果 list.forEach(System.out::println); } } ``` ### 測試 測試以上方法,在控制檯列印以下sql,我格式化了下,可以打看到 mybatis 幫我們拼接好了查詢條件。 ```sql SELECT DISTINCT e.id, e.`name`, e.gender, e.no, e.password , e.phone, e.address, e.status, e.deleted, e.department_id , e.gmt_create, e.gmt_modified FROM demo_employee e INNER JOIN demo_department d WHERE 1 = 1 AND e.gender = ? AND e.phone = ? AND e.address = ? AND e.deleted = ? AND d.no = ? ORDER BY name DESC ``` ## 關聯查詢 以上基本講完 IEmployeeRepository 中的方法,我前面說過,IEmployeeRepository 介面中的方法可以滿足大部分的使用需求,但是,如果我響應給前端的資料中,除了員工的欄位,還需要員工所在部門和員工擁有的角色的欄位,這時 IEmployeeRepository 的方法不就應付不了了嗎? 這種場景涉及到的就是關聯查詢,我需要在 repository 層查詢員工物件時將部門和角色一併查出來,然後在前端轉換為具體的 VO 物件。 ### 修改實體類 在員工的實體增加以下兩個屬性,專案中也可以建立一個 Employee 的子類,將關聯的屬性放入到子類裡,以便於管理。 ```java public class Employee { private Department department; private List roles = Collections.emptyList(); //······ } ``` ### 修改 mapper xml檔案 關聯查詢時,我們不需要修改原來的查詢語句,只需要修改 resultMap 就行。 ```java ``` 以上增加了兩個標籤,association 和 collection 分別用於配置一方和多方的關聯,其中 property 對應實體中的屬性,column 對應執行語句返回的欄位(如果沒有使用別名的話,一般為列名),select 指向了其他 mapper 語句的 id。 ### 編寫測試方法 我呼叫的還是 IEmployeeRepository 介面的 get 方法,只是增加了部門和角色的列印。 ```java @Test public void testGetRelation() { String id = "cc6b08506cdb11ea802000fffc35d9fe"; try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { // 執行,獲取員工物件 Employee employee = employeeRepository.get(id); // 列印員工 System.out.println(employee); // 列印部門 System.out.println(employee.getDepartment()); // 列印角色 employee.getRoles().forEach(System.out::println); } } ``` ### 測試 測試上面的方法,可以看到控制檯列印了三個查詢語句,分別對應員工、部門和角色,這樣,我們就可以在前端轉換 VO 時,將部門和角色的欄位都給到 VO 了。 ### 補充--延遲載入 上面的關聯查詢存在一個問題:如果我前端的 VO 只想要部門的欄位而不需要角色,或者我就只要員工的欄位,但是,這個時候還是會把部門和角色統統查出來,將會產生不必要的效能損耗。 這種情況,就需要使用延遲載入了。延遲載入可以保證關聯物件只有在用到的時候才去執行資料庫查詢。 #### 配置延遲載入 Mybatis 延遲載入功能預設是不開啟的,但配置開啟也很簡單,只要在主配置檔案中增加: ```xml ``` #### 編寫測試方法 還是使用前面的方法,這裡我把 role 部分的程式碼註釋掉。 ```java @Test public void testGetLazy() { String id = "cc6b08506cdb11ea802000fffc35d9fe"; try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { // 執行,獲取員工物件 Employee employee = employeeRepository.get(id); // 列印員工 System.out.println(employee); // 列印部門 System.out.println(employee.getDepartment()); // 列印角色 // employee.getRoles().forEach(System.out::println); } } ``` #### 測試 測試上面的方法,可以看到只關聯查出了部門,角色並沒有查出來,因為我們程式程式碼中沒有觸發 getRoles() 或 lazyLoadTriggerMethods 的操作。實際專案中,我們在前端轉換 VO 時,如果需要用到關聯物件的欄位,才會觸發查詢。 ## 分頁查詢 最後再簡單查下分頁查詢,這裡使用 pagehelper 外掛來實現。 ### 引入外掛依賴 在專案 pom.xml 檔案中增加以下依賴。 ```xml com.github.pagehelper pagehelper 5.1.10 ``` ### 修改 mybatis 主配置檔案 在 mybatis 主配置檔案中增加 plugins 元素,並引入分頁外掛。 ```xml ``` ### 編寫測試方法 ```java @Test public void testlistPage() { EmployeeCondition con = new EmployeeCondition(); // 設定條件 con.setGender(false); con.setAddress("北京"); con.setDeleted(false); con.setPhone("18826****41"); con.setDistinct(true); try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { // 設定分頁資訊 PageHelper.startPage(0, 3); // 執行查詢 List list = employeeRepository.list(con); // 封裝分頁模型 PageInfo pageInfo = new PageInfo<>(list); // 取分頁模型的資料 System.out.println("查詢總數" + pageInfo.getTotal()); } } ``` ### 測試 測試上面的方法,可以看到先進行了總數的查詢,再進行分頁查詢。 ```sql 2020-03-31 11:06:59.618 c.z.m.m.EmployeeMapper.selectByCondition_COUNT - ==> Preparing: SELECT COUNT(0) FROM ( SELECT DISTINCT e.id, e.`name`, e.gender, e.no, e.password , e.phone, e.address, e.status, e.deleted, e.department_id , e.gmt_create, e.gmt_modified FROM demo_employee e WHERE 1 = 1 AND e.gender = ? AND e.phone = ? AND e.address = ? AND e.deleted = ? ) table_count 2020-03-31 11:06:59.646 c.z.m.m.EmployeeMapper.selectByCondition_COUNT - ==> Parameters: false(Boolean), 18826****41(String), 北京(String), false(Boolean) 2020-03-31 11:06:59.678 c.z.m.m.EmployeeMapper.selectByCondition_COUNT - <== Total: 1 2020-03-31 11:06:59.693 c.z.m.mapper.EmployeeMapper.selectByCondition - ==> Preparing: SELECT DISTINCT e.id, e.`name`, e.gender, e.no, e.password , e.phone, e.address, e.status, e.deleted, e.department_id , e.gmt_create, e.gmt_modified FROM demo_employee e WHERE 1 = 1 AND e.gender = ? AND e.phone = ? AND e.address = ? AND e.deleted = ? LIMIT ? 2020-03-31 11:06:59.693 c.z.m.mapper.EmployeeMapper.selectByCondition - ==> Parameters: false(Boolean), 18826****41(String), 北京(String), false(Boolean), 3(Integer) 2020-03-31 11:06:59.724 c.z.m.mapper.EmployeeMapper.selectByCondition - <== Total: 3 查詢總數4 ``` # 參考資料 [Mybatis官方中文文件](https://Mybatis.org/Mybatis-3/zh/index.html/) > 相關原始碼請移步:[mybatis-demo](https://github.com/ZhangZiSheng001/mybatis-projects/tree/master/mybatis-demo) > 本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/12603885.html](https://www.cnblogs.com/ZhangZiSheng001/p/126038