MyBatis 應用的組成

王有志發表於2024-04-09

王有志,一個分享硬核Java技術的互金摸魚俠

加入Java人的提桶跑路群:共同富裕的Java人

大家好,我是王有志。在上一篇文章的最後,我們寫了一個簡單的例子,今天我們就透過這個例子來看一看一個標準的 MyBatis 應用程式由哪些元件組成。

最後,文末會解答小夥伴在私信中提出的問題:當存在多個 Mapper.xml,且每個檔案中都有同名的查詢語句時在 MyBatis 應用中該如何進行查詢?

Tips:文章中的示例,指的是《MyBatis 入門》正文中出現的簡單示例和附錄中“不使用 XML 構建 SqlSessionFactory”的例子,如無特別說明,預設為正文中的簡單示例。

MyBatis 應用的組成

我們先來回憶一下構建簡單示例的過程:

  1. 建立資料物件 UserDO,用於對映資料庫中的 user 表;
  2. 建立介面 UserDAO,作為 MyBatis 對映器的名稱空間;
  3. 建立對映器檔案 UserMapper.xml,並編寫了查詢全部 user 表資料的 SQL 語句
  4. 建立 MyBatis 的核心配置檔案 mybatis-config.xml,配置了資料庫資訊和對映器

以上的 4 步是我們在開始使用 MyBatis 前進行的前期配置工作,接下來是我們在應用程式中使用 MyBatis 的步驟:

  1. 透過 Resources 讀取 mybatis-config.xml 檔案,獲取 Reader 物件;
  2. 透過 Reader 物件構建出 SqlSessionFactory,即 SQL 會話工廠;
  3. 透過 SqlSessionFactory 獲取 SqlSession,即 SQL 會話
  4. 透過 SqlSession 執行 UserMapper.xml 中的 SQL 語句,並獲取到查詢結果。

這 4 步是我們在應用程式中使用 MyBatis 的過程,綜合以上兩步的內容我們大概可以構建出如下圖所示的 MyBatis 應用的基本組成:

圖中的部分元件已經在我們的示例中出現過了, 但是如 Executor,MappedStatement,Configuration,ResultHandler 等元件並沒有在我們的示例中出現。這是因為它們大都出現在 SqlSession 和 SqlSessionFactory 內部封裝的呼叫過程中,因此我們在平時使用時可能“見不到”它們,但這並不是說它們不重要,相反它們是 MyBatis 中起到關鍵作用的元件。

關於它們我還會在 MyBatis 系列的原始碼篇著重進行分析,不過在此之前,我會按照圖中自下向上的順序,逐一對這些元件的作用做一個簡單的說明。

Tips:Reader 是 Java 中 io 包下的工具類,因此在下文中不會出現關於 Reader 的內容。

Mapper.xml

Mapper.xml 是 MyBatis 的核心之一,是用於定義 SQL 語句和對映規則的 XML 檔案,由核心配置檔案 mybatis-config.xml 載入到 MyBaits 應用程式中。

Mapper.xml 的主要作用包括:

  • 定義 SQL 語句:MyBatis 的 SQL 語句編寫在 Mapper.xml 中(MyBatis 也支援透過註解的方式編寫 SQL 語句),透過 MyBatis 提供的 XML 標籤可以實現動態查詢條件和巢狀查詢等複雜的 SQL 語句;
  • 對映結果集到 Java 物件:透過 MyBatis 的標籤可以實現資料庫表中的欄位與 Java 物件中的欄位的對映關係,可以實現一對一,一對多等複雜關係的對映;
  • 介面方法繫結:Mapper.xml 中定義的 SQL 語句可以透過標籤中的 id 欄位與對應的 Mapper 介面中的方法進行繫結,透過呼叫 Mapper 介面的方法 MyBatis 將會執行 Mapper.xml 中的 SQL 語句。

下面我們對之前的示例稍作修改,來感受下 MyBatis 的中 Mapper.xml 與 Mapper 介面的方法繫結。

首先,在 UserMapper.xml 中定義一個新的查詢語句,用於查詢 id = 1 的使用者:

<select id="selectFirstUser" resultType="com.wyz.entity.UserDO" >
  select user_id, name, age, gender, id_type, id_number from user where user_id = 1
</select>

Tips:通常我們不會在 Mapper.xml 中編寫如user_id = 1這類硬編碼,這裡僅僅是為了舉例說明,千萬不要學~~~

接著我們修改 UserDAO 介面,新增兩個對應的方法宣告:

public interface UserMapper {

  List<UserDO> selectAll();

  UserDO selectFirstUser();
}

最後我們修改測試程式碼,透過 SqlSession 例項獲取 UseDAO 介面的例項,並呼叫介面中的方法:

@Test
public void test() {
	SqlSession sqlSession = sqlSessionFactory.openSession();
	UserDAO userDAO = sqlSession.getMapper(UserDAO.class);

	List<UserDO> users = userDAO.selectAll();
	for(UserDO user : users) {
		System.out.println(user.getName());
	}

	UserDO user = userDAO.selectFirstUser();
	System.out.println(user.getName());
}

可以看到,這裡我們透過 SqlSession 例項獲取到介面 UserDAO 的例項,分別呼叫了介面中的方法並能夠成功獲取到資料,這表明我們已經將 UserMapper.xml 中編寫的 SQL 語句與 UserDAO 介面中的方法繫結到了一起。

關於 Mapper.xml 的更多用法,我會在 MyBatis 系列的第 4 篇文章中和大家分享。

mybatis-config.xxml

mybatis-config.xml 是 MyBatis 應用中的核心配置檔案,該檔案中包含了 MyBatis 應用程式在執行時所需要的各種配置資訊。

示例中,我只做了最基礎的環境配置(資料庫事務管理器配置,資料來源配置)和對映器配置(載入對映器 UserMapper.xml),但實際上 mybatis-config.xml 中還提供了非常多的配置內容,如:別名配置(使用 typeAliases 標籤),外掛配置(使用 plugins 標籤)和物件工廠配置(使用 objectFactory 標籤)等等。

關於 mybatis-config.xml 的更多用法,我會在 MyBatis 系列的第 3 篇文章中和大家分享。

Resources

MyBatis 提供的資源載入工具,用於各種資原始檔的載入和訪問。Resources 提供了良好的封裝,使用起來非常簡單,只需要透過相對路徑,即可將資原始檔載入到應用程式中。

XMLConfigBuilder

XMLConfigBuilder 繼承自 BaseBuilder,負責解析 MyBatis 中的 XML 配置檔案(mybatis-config.xml),並透過呼叫XMLConfigBuilder#parse方法構建出 Configuration 物件。

BaseBuilder 有多個子類:

BaseBuilder 的子類分別負責解析不同的檔案,如:XMLConfigBuilder 負責解析 mybatis-config.xml 檔案,XMLMapperBuilder 負責解析 Mapper.xml 檔案等等。

Configuration

Configuration 是核心配置檔案 mybatis-config.xml 在 Java 應用程式中的體現,是 MyBatis 在整個執行週期中的配置資訊管理器,包含了 MyBatis 執行期間所需要的全部配置資訊和對映器。

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 使用了建造者模式,用來根據配置資訊生成 SqlSessionFactory。SqlSessionFactoryBuilder 提供了多個SqlSessionFactoryBuilder#build的過載方法,分別接受 Reader,InputStream 和 Configuration 三種方式輸入的配置資訊。

示例中,我們已經在SqlSessionFactoryBuilder#build方法中使用了 Reader 和 Configuration,而使用 InputStream 的方式與使用 Reader 的方式一模一樣,程式碼如下所示:

@BeforeClass
public static void init() throws IOException {
  InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  inputStream.close();
}

SqlSessionFactoryBuilder 的唯一作用是建立 SqlSessionFactory,當它完成了這個使命後,我們就應該毫不猶豫的拋棄它,因此 SqlSessionFactoryBuilder 應該作為方法內的區域性變數出現,生命週期僅在這個方法中,就像示例中的那樣。

SqlSessionFactory

SqlSessionFactory 是 MyBatis 中的介面,也是 MyBatis 的核心元件之一,SqlSessionFactory 使用了工廠方法,定義了 MyBatis 獲取 SqlSession 的規範。MyBatis 官方對於 SqlSessionFactory 的定位是每個 MyBatis 應用的核心:

每個基於 MyBatis 的應用都是以一個 SqlSessionFactory 的例項為核心的。

SqlSessionFactory 作為 MyBatis 應用程式中的核心,生命週期與整個 MyBatis 應用程式相同,隨著應用的建立而建立,應用的停止而銷燬。

SqlSessionFactory 有兩個實現類:

DefaultSqlSessionFactory 是 SqlSessionFactory 的預設實現類,用於獲取非執行緒安全的 SqlSession 例項,透過 DefaultSqlSessionFactory 獲取的 SqlSession 例項在使用時還需要手動關閉(同時會提交事務),即呼叫SqlSession#close方法。

SqlSessionFactory 介面提供了多個SqlSessionFactory#openSession的過載方法:

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);
}

使用無參的SqlSession#openSession方法可以獲取具有如下特性的 SqlSession 例項:

  • 不會自動提交資料庫事務;
  • 透過 mybatis-config.xml 配置的資料來源獲取的 Connection 例項;
  • 使用資料來源預設的事務隔離級別;
  • 不會複用預處理語句,也不會批次進執行更新語句。

那麼對於其它SqlSession#openSession過載方法中的引數,我們能夠很輕鬆得想到它們的作用:

  • boolean autoCommit,設定 SqlSession 是否自動提交
  • Connection connection,設定 SqlSession 中使用的 Connection 例項(允許透過其它資料來源獲取)
  • TransactionIsolationLevel level,設定 SqlSession 中使用的事務隔離級別

至於 ExecutorType 引數,它是用來選擇 MyBatis 執行器的,MyBatis 中定義了 3 種型別的執行器:

  • ExecutorType#SIMPLE,該執行器會為每條 SQL 語句建立新的 PreparedStatement 例項;
  • ExecutorType#REUSE,該執行器會複用 PreparedStatement 例項;
  • ExecutorType#BATCH,該執行器會批次執行所有更新語句。

使用哪個SqlSession#openSession的過載方法,需要我們根據具體的業務場景來進行選擇。

Tips:因為 SqlSessionManager 同時也實現了 SqlSession 介面,而且在使用過程中更多的是作為 SqlSession 的實現而使用,所以我會將 SqlSessionManager 放在 SqlSession 的章節中進行說明。

SqlSession

SqlSession 是 MyBatis 的介面,同樣也是 MyBatis 的核心元件之一,定義了 MyBatis 與資料庫互動的規範,提供了執行 SQL 語句,提交/回滾事務以及獲取對映器(Mapper 介面)例項的方法。

SqlSession 有兩個實現類:

DefaultSqlSession 是 SqlSession 的預設實現類,透過 DefaultSqlSessionFactory 獲取

DefaultSqlSession 與 SqlSessionManager 的主要區別體現在兩個方面:

  • 執行緒安全:
    • DefaultSqlSession 不是執行緒安全的 SqlSession 例項(也可以說是透過 DefaultSqlSessionFactory 獲取的 SqlSession 例項不是執行緒安全的)
    • SqlSessionManager 提供了執行緒安全的 SqlSession 例項
  • 事務管理:
    • DefaultSqlSession 需要手動提交事務,或者在執行SqlSession#close方法時自動提交事務
    • 透過 SqlSessionManager 執行 SQL 語句時,會自動的進行事務提交。

SqlSession 例項的生命週期對應一次資料庫會話,當我們透過 SqlSessionFactory 獲取 SqlSession 例項時是 SqlSession 生命週期的開始,而我們呼叫SqlSession#close方法後,是 SqlSession 例項的生命週期的結束,這期間的過程通常對應著一項業務操作從開始到結束的過程,因此我們可以認為 SqlSession 例項的生命週期是一次業務操作從開始到結束的時間

特別提醒,雖然每個 SqlSession 例項都有與之對應的 Connection 例項,且資料庫互動是由 Connection 例項完成的,但由於資料庫連線池的存在,呼叫SqlSession#close方法後,SqlSession 例項只是將 Connection 例項“歸還”到資料庫連線池中,而不是呼叫Connection#close來關閉 Connection 例項,因此我們不能將 SqlSession 例項的生命週期與 Connection 例項的生命週期畫上等號。

Tips:透過 SqlSession 執行 SQL 語句是 iBATIS 時代的用法,在當下的環境中,特別是在 MyBatis 與 Spring 整合後,我們通常會選擇透過 SqlSession 例項獲取對映器例項後直接呼叫介面方法,即在文章開頭中解釋對映器介面方法繫結時的使用方式。

Executor

Executor 是 MyBatis 中的介面,同樣是 MyBatis 中的核心元件。Executor 介面定義了 MyBatis 與資料庫互動的規範。不同 Executor 的實現類提供了不同的特性,例如:SimpleExecutor 每次都會建立 PreparedStatement 物件,ReuseExecutor 會複用已經存在的 PreparedStatement 物件,BatchExecutor 用於批次執行 SQL 更新語句,CachingExecutor 提供了查詢結果的快取能力。

MyBatis 中 Executor 的體系結構如下:

關於 Executor 體系的中各實現類的具體作用與功能,我會在 MyBatis 系列的後續文章中繼續和大家分享。

MappedStatement

MappedStatement 中封裝了 Mapper.xml 檔案中對映的 SQL 語句資訊,包括 SQL 語句的 id,SQL 語句,引數對映資訊,結果集對映資訊,以及快取策略等。

StatementHandler

StatementHandler 是 MyBatis 中的介面,負責 MyBatis 中的 SQL 處理,如預編譯,引數設定,SQL 語句執行等。

MyBatis 中 StatementHandler 的體系結構如下:

ResultSetHandler

ResultHandler 是 MyBatis 中的介面,依舊是 MyBatis 中的核心元件。ResultHandler 只有一個實現類 DefaultResultSetHandler,負責將資料庫返回的結果集對映為 Java 物件,需要注意的是 ResultSetHandler 與 ResultHandler 是不同的,ResultSetHandler 負責 MyBatis 內部將結果集對映為 Java 物件,而 ResultHandler 提供了對結果集資料的二次處理能力,允許開發者進行自定義,會在 ResultSetHandler 處理完結果集的對映後呼叫ResultHandler#handlerResult方法。

問題答疑

上一篇文章中,我們只配置了一個 UserMapper.xml,因此有些小夥伴產生了迷惑,當存在多個 Mapper.xml,且每個檔案中都有同名的查詢語句時在 MyBatis 應用中該如何進行查詢?

一句話概括就是透過 namespace + id 方式來關聯到唯一的 SQL 語句對映上。類似於,當 Java 應用程式中存在多個同名 Java 類時,我們可以透過全限名的方式訪問不同的 Java 類。

我們先隨便建一個表,SQL 語句如下:

create table company (
  id              int          not null primary key,
  company_name    varchar(50)  not null,
  company_address varchar(500) not null
);

接著按照上篇文章中的方式分別建立 company 表對應的 CompanyDO,CompanyDAO 和 CompanyMapper.xml,其中 CompanyMapper.xml 需要定義與 UserMapper.xml 中同名的查詢語句,CompanyMapper.xml 內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wyz.dao.CompanyDAO">
    <select id="selectAll" resultType="com.wyz.entity.CompanyDO" >
        select id, company_name, company_address from company
    </select>
</mapper>

接著我們修改 mybatis-config.xml 檔案,新增對映檔案 CompanyMapper.xml:

<configuration>
  <!-- 省略資料庫配置的部分 -->

  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
    <mapper resource="mapper/CompanyMapper.xml"/>
  </mappers>
</configuration>

最後我們修改測試程式碼:

@Test
public void testSelectAll() {
  SqlSession sqlSession = sqlSessionFactory.openSession();
  List<UserDO> users = sqlSession.selectList("com.wyz.dao.UserDAO.selectAll");
  for(UserDO userDo:users) {
    log.info(userDo.getName());
  }
}

這樣我們就可以透過 namespace + id 的方式對映到指定的 SQL 語句了


好了,今天的內容就到這裡了,如果本文對你有幫助的話,希望多多點贊支援,如果文章中出現任何錯誤,還請批評指正。最後歡迎大家關注分享硬核 Java 技術的金融摸魚俠王有志,我們下次再見!

相關文章