1. 程式人生 > 實用技巧 >Mybatis-3官網

Mybatis-3官網

注:官網炸裂看這裡就好,(複製官網)

入門

安裝

要使用 MyBatis, 只需將 mybatis-x.x.x.jar 檔案置於類路徑(classpath)中即可。

如果使用 Maven 來構建專案,則需將下面的依賴程式碼置於 pom.xml 檔案中:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

從 XML 中構建 SqlSessionFactory

每個基於 MyBatis 的應用都是以一個 SqlSessionFactory 的例項為核心的。SqlSessionFactory 的例項可以通過 SqlSessionFactoryBuilder 獲得。而 SqlSessionFactoryBuilder 則可以從 XML 配置檔案或一個預先配置的 Configuration 例項來構建出 SqlSessionFactory 例項。

從 XML 檔案中構建 SqlSessionFactory 的例項非常簡單,建議使用類路徑下的資原始檔進行配置。 但也可以使用任意的輸入流(InputStream)例項,比如用檔案路徑字串或 file:// URL 構造的輸入流。MyBatis 包含一個名叫 Resources 的工具類,它包含一些實用方法,使得從類路徑或其它位置載入資原始檔更加容易。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

XML 配置檔案中包含了對 MyBatis 系統的核心設定,包括獲取資料庫連線例項的資料來源(DataSource)以及決定事務作用域和控制方式的事務管理器(TransactionManager)。後面會再探討 XML 配置檔案的詳細內容,這裡先給出一個簡單的示例:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

當然,還有很多可以在 XML 檔案中配置的選項,上面的示例僅羅列了最關鍵的部分。 注意 XML 頭部的宣告,它用來驗證 XML 文件的正確性。environment 元素體中包含了事務管理和連線池的配置。mappers 元素則包含了一組對映器(mapper),這些對映器的 XML 對映檔案包含了 SQL 程式碼和對映定義資訊。

不使用 XML 構建 SqlSessionFactory

如果你更願意直接從 Java 程式碼而不是 XML 檔案中建立配置,或者想要建立你自己的配置建造器,MyBatis 也提供了完整的配置類,提供了所有與 XML 檔案等價的配置項。

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

注意該例中,configuration 添加了一個對映器類(mapper class)。對映器類是 Java 類,它們包含 SQL 對映註解從而避免依賴 XML 檔案。不過,由於 Java 註解的一些限制以及某些 MyBatis 對映的複雜性,要使用大多數高階對映(比如:巢狀聯合對映),仍然需要使用 XML 配置。有鑑於此,如果存在一個同名 XML 配置檔案,MyBatis 會自動查詢並載入它(在這個例子中,基於類路徑和 BlogMapper.class 的類名,會載入 BlogMapper.xml)。具體細節稍後討論。

從 SqlSessionFactory 中獲取 SqlSession

既然有了 SqlSessionFactory,顧名思義,我們可以從中獲得 SqlSession 的例項。SqlSession 提供了在資料庫執行 SQL 命令所需的所有方法。你可以通過 SqlSession 例項來直接執行已對映的 SQL 語句。例如:

try (SqlSession session = sqlSessionFactory.openSession()) {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

誠然,這種方式能夠正常工作,對使用舊版本 MyBatis 的使用者來說也比較熟悉。但現在有了一種更簡潔的方式——使用和指定語句的引數和返回值相匹配的介面(比如 BlogMapper.class),現在你的程式碼不僅更清晰,更加型別安全,還不用擔心可能出錯的字串字面值以及強制型別轉換。

例如:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
}

現在我們來探究一下這段程式碼究竟做了些什麼。

探究已對映的 SQL 語句

現在你可能很想知道 SqlSession 和 Mapper 到底具體執行了些什麼操作,但 SQL 語句對映是個相當廣泛的話題,可能會佔去文件的大部分篇幅。 但為了讓你能夠了解個大概,這裡會給出幾個例子。

在上面提到的例子中,一個語句既可以通過 XML 定義,也可以通過註解定義。我們先看看 XML 定義語句的方式,事實上 MyBatis 提供的所有特性都可以利用基於 XML 的對映語言來實現,這使得 MyBatis 在過去的數年間得以流行。如果你用過舊版本的 MyBatis,你應該對這個概念比較熟悉。 但相比於之前的版本,新版本改進了許多 XML 的配置,後面我們會提到這些改進。這裡給出一個基於 XML 對映語句的示例,它應該可以滿足上個示例中 SqlSession 的呼叫。

<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

為了這個簡單的例子,我們似乎寫了不少配置,但其實並不多。在一個 XML 對映檔案中,可以定義無數個對映語句,這樣一來,XML 頭部和文件型別宣告部分就顯得微不足道了。文件的其它部分很直白,容易理解。 它在名稱空間 “org.mybatis.example.BlogMapper” 中定義了一個名為 “selectBlog” 的對映語句,這樣你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 來呼叫對映語句了,就像上面例子中那樣:

Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);

你可能會注意到,這種方式和用全限定名呼叫 Java 物件的方法類似。這樣,該命名就可以直接對映到在名稱空間中同名的對映器類,並將已對映的 select 語句匹配到對應名稱、引數和返回型別的方法。因此你就可以像上面那樣,不費吹灰之力地在對應的對映器介面呼叫方法,就像下面這樣:

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

第二種方法有很多優勢,首先它不依賴於字串字面值,會更安全一點;其次,如果你的 IDE 有程式碼補全功能,那麼程式碼補全可以幫你快速選擇到對映好的 SQL 語句。


提示 對名稱空間的一點補充

在之前版本的 MyBatis 中,名稱空間(Namespaces)的作用並不大,是可選的。 但現在,隨著名稱空間越發重要,你必須指定名稱空間。

名稱空間的作用有兩個,一個是利用更長的全限定名來將不同的語句隔離開來,同時也實現了你上面見到的介面繫結。就算你覺得暫時用不到介面繫結,你也應該遵循這裡的規定,以防哪天你改變了主意。 長遠來看,只要將名稱空間置於合適的 Java 包名稱空間之中,你的程式碼會變得更加整潔,也有利於你更方便地使用 MyBatis。

命名解析:為了減少輸入量,MyBatis 對所有具有名稱的配置元素(包括語句,結果對映,快取等)使用瞭如下的命名解析規則。

  • 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)將被直接用於查詢及使用。
  • 短名稱(比如 “selectAllThings”)如果全域性唯一也可以作為一個單獨的引用。 如果不唯一,有兩個或兩個以上的相同名稱(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那麼使用時就會產生“短名稱不唯一”的錯誤,這種情況下就必須使用全限定名。

對於像 BlogMapper 這樣的對映器類來說,還有另一種方法來完成語句對映。 它們對映的語句可以不用 XML 來配置,而可以使用 Java 註解來配置。比如,上面的 XML 示例可以被替換成如下的配置:

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

使用註解來對映簡單語句會使程式碼顯得更加簡潔,但對於稍微複雜一點的語句,Java 註解不僅力不從心,還會讓你本就複雜的 SQL 語句更加混亂不堪。 因此,如果你需要做一些很複雜的操作,最好用 XML 來對映語句。

選擇何種方式來配置對映,以及認為是否應該要統一對映語句定義的形式,完全取決於你和你的團隊。 換句話說,永遠不要拘泥於一種方式,你可以很輕鬆的在基於註解和 XML 的語句對映方式間自由移植和切換。

作用域(Scope)和生命週期

理解我們之前討論過的不同作用域和生命週期類別是至關重要的,因為錯誤的使用會導致非常嚴重的併發問題。


提示 物件生命週期和依賴注入框架

依賴注入框架可以建立執行緒安全的、基於事務的 SqlSession 和對映器,並將它們直接注入到你的 bean 中,因此可以直接忽略它們的生命週期。 如果對如何通過依賴注入框架使用 MyBatis 感興趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 兩個子專案。


SqlSessionFactoryBuilder

這個類可以被例項化、使用和丟棄,一旦建立了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 例項的最佳作用域是方法作用域(也就是區域性方法變數)。 你可以重用 SqlSessionFactoryBuilder 來建立多個 SqlSessionFactory 例項,但最好還是不要一直保留著它,以保證所有的 XML 解析資源可以被釋放給更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被建立就應該在應用的執行期間一直存在,沒有任何理由丟棄它或重新建立另一個例項。 使用 SqlSessionFactory 的最佳實踐是在應用執行期間不要重複建立多次,多次重建 SqlSessionFactory 被視為一種程式碼“壞習慣”。因此 SqlSessionFactory 的最佳作用域是應用作用域。 有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。

SqlSession

每個執行緒都應該有它自己的 SqlSession 例項。SqlSession 的例項不是執行緒安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。 絕對不能將 SqlSession 例項的引用放在一個類的靜態域,甚至一個類的例項變數也不行。 也絕不能將 SqlSession 例項的引用放在任何型別的託管作用域中,比如 Servlet 框架中的 HttpSession。 如果你現在正在使用一種 Web 框架,考慮將 SqlSession 放在一個和 HTTP 請求相似的作用域中。 換句話說,每次收到 HTTP 請求,就可以開啟一個 SqlSession,返回一個響應後,就關閉它。 這個關閉操作很重要,為了確保每次都能執行關閉操作,你應該把這個關閉操作放到 finally 塊中。 下面的示例就是一個確保 SqlSession 關閉的標準模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的應用邏輯程式碼
}

在所有程式碼中都遵循這種使用模式,可以保證所有資料庫資源都能被正確地關閉。

對映器例項

對映器是一些繫結對映語句的介面。對映器介面的例項是從 SqlSession 中獲得的。雖然從技術層面上來講,任何對映器例項的最大作用域與請求它們的 SqlSession 相同。但方法作用域才是對映器例項的最合適的作用域。 也就是說,對映器例項應該在呼叫它們的方法中被獲取,使用完畢之後即可丟棄。 對映器例項並不需要被顯式地關閉。儘管在整個請求作用域保留對映器例項不會有什麼問題,但是你很快會發現,在這個作用域上管理太多像 SqlSession 的資源會讓你忙不過來。 因此,最好將對映器放在方法作用域內。就像下面的例子一樣:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的應用邏輯程式碼
}

XML 配置

MyBatis 的配置檔案包含了會深深影響 MyBatis 行為的設定和屬性資訊。 配置文件的頂層結構如下:

屬性(properties)

這些屬性可以在外部進行配置,並可以進行動態替換。你既可以在典型的 Java 屬性檔案中配置這些屬性,也可以在 properties 元素的子元素中設定。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

設定好的屬性可以在整個配置檔案中用來替換需要動態配置的屬性值。比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

這個例子中的 username 和 password 將會由 properties 元素中設定的相應值來替換。 driver 和 url 屬性將會由 config.properties 檔案中對應的值來替換。這樣就為配置提供了諸多靈活選擇。

也可以在 SqlSessionFactoryBuilder.build() 方法中傳入屬性值。例如:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);

// ... 或者 ...

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);

如果一個屬性在不只一個地方進行了配置,那麼,MyBatis 將按照下面的順序來載入:

  • 首先讀取在 properties 元素體內指定的屬性。
  • 然後根據 properties 元素中的 resource 屬性讀取類路徑下屬性檔案,或根據 url 屬性指定的路徑讀取屬性檔案,並覆蓋之前讀取過的同名屬性。
  • 最後讀取作為方法引數傳遞的屬性,並覆蓋之前讀取過的同名屬性。

因此,通過方法引數傳遞的屬性具有最高優先順序,resource/url 屬性中指定的配置檔案次之,最低優先順序的則是 properties 元素中指定的屬性。

從 MyBatis 3.4.2 開始,你可以為佔位符指定一個預設值。例如:

<dataSource type="POOLED">
  <!-- ... -->
  <property name="username" value="${username:ut_user}"/> <!-- 如果屬性 'username' 沒有被配置,'username' 屬性的值將為 'ut_user' -->
</dataSource>

這個特性預設是關閉的。要啟用這個特性,需要新增一個特定的屬性來開啟這個特性。例如:

<properties resource="org/mybatis/example/config.properties">
  <!-- ... -->
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- 啟用預設值特性 -->
</properties>

提示 如果你在屬性名中使用了 ":" 字元(如:db:username),或者在 SQL 對映中使用了 OGNL 表示式的三元運算子(如: ${tableName != null ? tableName : 'global_constants'}),就需要設定特定的屬性來修改分隔屬性名和預設值的字元。例如:

<properties resource="org/mybatis/example/config.properties">
  <!-- ... -->
  <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> <!-- 修改預設值的分隔符 -->
</properties>
<dataSource type="POOLED">
  <!-- ... -->
  <property name="username" value="${db:username?:ut_user}"/>
</dataSource>

設定(settings)

這是 MyBatis 中極為重要的調整設定,它們會改變 MyBatis 的執行時行為。 下表描述了設定中各項設定的含義、預設值等。

設定名 描述 有效值 預設值
cacheEnabled 全域性性地開啟或關閉所有對映器配置檔案中已配置的任何快取。 true | false true
lazyLoadingEnabled 延遲載入的全域性開關。當開啟時,所有關聯物件都會延遲載入。 特定關聯關係中可通過設定 fetchType 屬性來覆蓋該項的開關狀態。 true | false false
aggressiveLazyLoading 開啟時,任一方法的呼叫都會載入該物件的所有延遲載入屬性。 否則,每個延遲載入屬性會按需載入(參考 lazyLoadTriggerMethods)。 true | false false (在 3.4.1 及之前的版本中預設為 true)
multipleResultSetsEnabled 是否允許單個語句返回多結果集(需要資料庫驅動支援)。 true | false true
useColumnLabel 使用列標籤代替列名。實際表現依賴於資料庫驅動,具體可參考資料庫驅動的相關文件,或通過對比測試來觀察。 true | false true
useGeneratedKeys 允許 JDBC 支援自動生成主鍵,需要資料庫驅動支援。如果設定為 true,將強制使用自動生成主鍵。儘管一些資料庫驅動不支援此特性,但仍可正常工作(如 Derby)。 true | false False
autoMappingBehavior 指定 MyBatis 應如何自動對映列到欄位或屬性。 NONE 表示關閉自動對映;PARTIAL 只會自動對映沒有定義巢狀結果對映的欄位。 FULL 會自動對映任何複雜的結果集(無論是否巢狀)。 NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定發現自動對映目標未知列(或未知屬性型別)的行為。NONE: 不做任何反應WARNING: 輸出警告日誌('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日誌等級必須設定為 WARNFAILING: 對映失敗 (丟擲 SqlSessionException) NONE, WARNING, FAILING NONE
defaultExecutorType 配置預設的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(PreparedStatement); BATCH 執行器不僅重用語句還會執行批量更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設定超時時間,它決定資料庫驅動等待資料庫響應的秒數。 任意正整數 未設定 (null)
defaultFetchSize 為驅動的結果集獲取數量(fetchSize)設定一個建議值。此引數只可以在查詢設定中被覆蓋。 任意正整數 未設定 (null)
defaultResultSetType 指定語句預設的滾動策略。(新增於 3.5.2) FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同於未設定) 未設定 (null)
safeRowBoundsEnabled 是否允許在巢狀語句中使用分頁(RowBounds)。如果允許使用則設定為 false。 true | false False
safeResultHandlerEnabled 是否允許在巢狀語句中使用結果處理器(ResultHandler)。如果允許使用則設定為 false。 true | false True
mapUnderscoreToCamelCase 是否開啟駝峰命名自動對映,即從經典資料庫列名 A_COLUMN 對映到經典 Java 屬性名 aColumn。 true | false False
localCacheScope MyBatis 利用本地快取機制(Local Cache)防止迴圈引用和加速重複的巢狀查詢。 預設值為 SESSION,會快取一個會話中執行的所有查詢。 若設定值為 STATEMENT,本地快取將僅用於執行語句,對相同 SqlSession 的不同查詢將不會進行快取。 SESSION | STATEMENT SESSION
jdbcTypeForNull 當沒有為引數指定特定的 JDBC 型別時,空值的預設 JDBC 型別。 某些資料庫驅動需要指定列的 JDBC 型別,多數情況直接用一般型別即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 OTHER
lazyLoadTriggerMethods 指定物件的哪些方法觸發一次延遲載入。 用逗號分隔的方法列表。 equals,clone,hashCode,toString
defaultScriptingLanguage 指定動態 SQL 生成使用的預設指令碼語言。 一個類型別名或全限定類名。 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
defaultEnumTypeHandler 指定 Enum 使用的預設 TypeHandler 。(新增於 3.4.5) 一個類型別名或全限定類名。 org.apache.ibatis.type.EnumTypeHandler
callSettersOnNulls 指定當結果集中值為 null 的時候是否呼叫對映物件的 setter(map 物件時為 put)方法,這在依賴於 Map.keySet() 或 null 值進行初始化時比較有用。注意基本型別(int、boolean 等)是不能設定成 null 的。 true | false false
returnInstanceForEmptyRow 當返回行的所有列都是空時,MyBatis預設返回 null。 當開啟這個設定時,MyBatis會返回一個空例項。 請注意,它也適用於巢狀的結果集(如集合或關聯)。(新增於 3.4.2) true | false false
logPrefix 指定 MyBatis 增加到日誌名稱的字首。 任何字串 未設定
logImpl 指定 MyBatis 所用日誌的具體實現,未指定時將自動查詢。 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未設定
proxyFactory 指定 Mybatis 建立可延遲載入物件所用到的代理工具。 CGLIB | JAVASSIST JAVASSIST (MyBatis 3.3 以上)
vfsImpl 指定 VFS 的實現 自定義 VFS 的實現的類全限定名,以逗號分隔。 未設定
useActualParamName 允許使用方法簽名中的名稱作為語句引數名稱。 為了使用該特性,你的專案必須採用 Java 8 編譯,並且加上 -parameters 選項。(新增於 3.4.1) true | false true
configurationFactory 指定一個提供 Configuration 例項的類。 這個被返回的 Configuration 例項用來載入被反序列化物件的延遲載入屬性值。 這個類必須包含一個簽名為static Configuration getConfiguration() 的方法。(新增於 3.2.3) 一個類型別名或完全限定類名。 未設定
shrinkWhitespacesInSql 從SQL中刪除多餘的空格字元。請注意,這也會影響SQL中的文字字串。 (新增於 3.5.5) true | false false
defaultSqlProviderType Specifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the type(or value) attribute on sql provider annotation(e.g. @SelectProvider), when these attribute was omitted. A type alias or fully qualified class name Not set

一個配置完整的 settings 元素的示例如下:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

類型別名(typeAliases)

類型別名可為 Java 型別設定一個縮寫名字。 它僅用於 XML 配置,意在降低冗餘的全限定類名書寫。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

當這樣配置時,Blog 可以用在任何使用 domain.blog.Blog 的地方。

也可以指定一個包名,MyBatis 會在包名下面搜尋需要的 Java Bean,比如:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

每一個在包 domain.blog 中的 Java Bean,在沒有註解的情況下,會使用 Bean 的首字母小寫的非限定類名來作為它的別名。 比如 domain.blog.Author 的別名為 author;若有註解,則別名為其註解值。見下面的例子:

@Alias("author")
public class Author {
    ...
}

下面是一些為常見的 Java 型別內建的類型別名。它們都是不區分大小寫的,注意,為了應對原始型別的命名重複,採取了特殊的命名風格。

別名 對映的型別
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

型別處理器(typeHandlers)

MyBatis 在設定預處理語句(PreparedStatement)中的引數或從結果集中取出一個值時, 都會用型別處理器將獲取到的值以合適的方式轉換成 Java 型別。下表描述了一些預設的型別處理器。

提示 從 3.4.5 開始,MyBatis 預設支援 JSR-310(日期和時間 API) 。

型別處理器 Java 型別 JDBC 型別
BooleanTypeHandler java.lang.Boolean, boolean 資料庫相容的 BOOLEAN
ByteTypeHandler java.lang.Byte, byte 資料庫相容的 NUMERICBYTE
ShortTypeHandler java.lang.Short, short 資料庫相容的 NUMERICSMALLINT
IntegerTypeHandler java.lang.Integer, int 資料庫相容的 NUMERICINTEGER
LongTypeHandler java.lang.Long, long 資料庫相容的 NUMERICBIGINT
FloatTypeHandler java.lang.Float, float 資料庫相容的 NUMERICFLOAT
DoubleTypeHandler java.lang.Double, double 資料庫相容的 NUMERICDOUBLE
BigDecimalTypeHandler java.math.BigDecimal 資料庫相容的 NUMERICDECIMAL
StringTypeHandler java.lang.String CHAR, VARCHAR
ClobReaderTypeHandler java.io.Reader -
ClobTypeHandler java.lang.String CLOB, LONGVARCHAR
NStringTypeHandler java.lang.String NVARCHAR, NCHAR
NClobTypeHandler java.lang.String NCLOB
BlobInputStreamTypeHandler java.io.InputStream -
ByteArrayTypeHandler byte[] 資料庫相容的位元組流型別
BlobTypeHandler byte[] BLOB, LONGVARBINARY
DateTypeHandler java.util.Date TIMESTAMP
DateOnlyTypeHandler java.util.Date DATE
TimeOnlyTypeHandler java.util.Date TIME
SqlTimestampTypeHandler java.sql.Timestamp TIMESTAMP
SqlDateTypeHandler java.sql.Date DATE
SqlTimeTypeHandler java.sql.Time TIME
ObjectTypeHandler Any OTHER 或未指定型別
EnumTypeHandler Enumeration Type VARCHAR 或任何相容的字串型別,用來儲存列舉的名稱(而不是索引序數值)
EnumOrdinalTypeHandler Enumeration Type 任何相容的 NUMERICDOUBLE 型別,用來儲存列舉的序數值(而不是名稱)。
SqlxmlTypeHandler java.lang.String SQLXML
InstantTypeHandler java.time.Instant TIMESTAMP
LocalDateTimeTypeHandler java.time.LocalDateTime TIMESTAMP
LocalDateTypeHandler java.time.LocalDate DATE
LocalTimeTypeHandler java.time.LocalTime TIME
OffsetDateTimeTypeHandler java.time.OffsetDateTime TIMESTAMP
OffsetTimeTypeHandler java.time.OffsetTime TIME
ZonedDateTimeTypeHandler java.time.ZonedDateTime TIMESTAMP
YearTypeHandler java.time.Year INTEGER
MonthTypeHandler java.time.Month INTEGER
YearMonthTypeHandler java.time.YearMonth VARCHARLONGVARCHAR
JapaneseDateTypeHandler java.time.chrono.JapaneseDate DATE

你可以重寫已有的型別處理器或建立你自己的型別處理器來處理不支援的或非標準的型別。 具體做法為:實現 org.apache.ibatis.type.TypeHandler 介面, 或繼承一個很便利的類 org.apache.ibatis.type.BaseTypeHandler, 並且可以(可選地)將它對映到一個 JDBC 型別。比如:

// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
  }

  @Override
  public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return rs.getString(columnName);
  }

  @Override
  public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    return rs.getString(columnIndex);
  }

  @Override
  public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    return cs.getString(columnIndex);
  }
}
<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

使用上述的型別處理器將會覆蓋已有的處理 Java String 型別的屬性以及 VARCHAR 型別的引數和結果的型別處理器。 要注意 MyBatis 不會通過檢測資料庫元資訊來決定使用哪種型別,所以你必須在引數和結果對映中指明欄位是 VARCHAR 型別, 以使其能夠繫結到正確的型別處理器上。這是因為 MyBatis 直到語句被執行時才清楚資料型別。

通過型別處理器的泛型,MyBatis 可以得知該型別處理器處理的 Java 型別,不過這種行為可以通過兩種方法改變:

  • 在型別處理器的配置元素(typeHandler 元素)上增加一個 javaType 屬性(比如:javaType="String");
  • 在型別處理器的類上增加一個 @MappedTypes 註解指定與其關聯的 Java 型別列表。 如果在 javaType 屬性中也同時指定,則註解上的配置將被忽略。

可以通過兩種方式來指定關聯的 JDBC 型別:

  • 在型別處理器的配置元素上增加一個 jdbcType 屬性(比如:jdbcType="VARCHAR");
  • 在型別處理器的類上增加一個 @MappedJdbcTypes 註解指定與其關聯的 JDBC 型別列表。 如果在 jdbcType 屬性中也同時指定,則註解上的配置將被忽略。

當在 ResultMap 中決定使用哪種型別處理器時,此時 Java 型別是已知的(從結果型別中獲得),但是 JDBC 型別是未知的。 因此 Mybatis 使用 javaType=[Java 型別], jdbcType=null 的組合來選擇一個型別處理器。 這意味著使用 @MappedJdbcTypes 註解可以限制型別處理器的作用範圍,並且可以確保,除非顯式地設定,否則型別處理器在 ResultMap 中將不會生效。 如果希望能在 ResultMap 中隱式地使用型別處理器,那麼設定 @MappedJdbcTypes 註解的 includeNullJdbcType=true 即可。 然而從 Mybatis 3.4.0 開始,如果某個 Java 型別只有一個註冊的型別處理器,即使沒有設定 includeNullJdbcType=true,那麼這個型別處理器也會是 ResultMap 使用 Java 型別時的預設處理器。

最後,可以讓 MyBatis 幫你查詢型別處理器:

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

注意在使用自動發現功能的時候,只能通過註解方式來指定 JDBC 的型別。

你可以建立能夠處理多個類的泛型型別處理器。為了使用泛型型別處理器, 需要增加一個接受該類的 class 作為引數的構造器,這樣 MyBatis 會在構造一個型別處理器例項的時候傳入一個具體的類。

//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {

  private Class<E> type;

  public GenericTypeHandler(Class<E> type) {
    if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
    this.type = type;
  }
  ...

EnumTypeHandlerEnumOrdinalTypeHandler 都是泛型型別處理器,我們將會在接下來的部分詳細探討。

處理列舉型別

若想對映列舉型別 Enum,則需要從 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中選擇一個來使用。

比如說我們想儲存取近似值時用到的舍入模式。預設情況下,MyBatis 會利用 EnumTypeHandler 來把 Enum 值轉換成對應的名字。

注意 EnumTypeHandler 在某種意義上來說是比較特別的,其它的處理器只針對某個特定的類,而它不同,它會處理任意繼承了 Enum 的類。

不過,我們可能不想儲存名字,相反我們的 DBA 會堅持使用整形值程式碼。那也一樣簡單:在配置檔案中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可, 這樣每個 RoundingMode 將通過他們的序數值來對映成對應的整形數值。

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>

但要是你想在一個地方將 Enum 對映成字串,在另外一個地方對映成整形值呢?

自動對映器(auto-mapper)會自動地選用 EnumOrdinalTypeHandler 來處理列舉型別, 所以如果我們想用普通的 EnumTypeHandler,就必須要顯式地為那些 SQL 語句設定要使用的型別處理器。

(下一節才開始介紹對映器檔案,如果你是首次閱讀該文件,你可能需要先跳過這裡,過會再來看。)

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode"/>
	</resultMap>

	<select id="getUser" resultMap="usermap">
		select * from users
	</select>
	<insert id="insert">
	    insert into users (id, name, funkyNumber, roundingMode) values (
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode}
	    )
	</insert>

	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
	</resultMap>
	<select id="getUser2" resultMap="usermap2">
		select * from users2
	</select>
	<insert id="insert2">
	    insert into users2 (id, name, funkyNumber, roundingMode) values (
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}
	    )
	</insert>

</mapper>

注意,這裡的 select 語句必須指定 resultMap 而不是 resultType

物件工廠(objectFactory)

每次 MyBatis 建立結果物件的新例項時,它都會使用一個物件工廠(ObjectFactory)例項來完成例項化工作。 預設的物件工廠需要做的僅僅是例項化目標類,要麼通過預設無參構造方法,要麼通過存在的引數對映來呼叫帶有引數的構造方法。 如果想覆蓋物件工廠的預設行為,可以通過建立自己的物件工廠來實現。比如:

// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

ObjectFactory 介面很簡單,它包含兩個建立例項用的方法,一個是處理預設無參構造方法的,另外一個是處理帶引數的構造方法的。 另外,setProperties 方法可以被用來配置 ObjectFactory,在初始化你的 ObjectFactory 例項後, objectFactory 元素體中定義的屬性會被傳遞給 setProperties 方法。

外掛(plugins)

MyBatis 允許你在對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

這些類中方法的細節可以通過檢視每個方法的簽名來發現,或者直接檢視 MyBatis 發行包中的原始碼。 如果你想做的不僅僅是監控方法的呼叫,那麼你最好相當瞭解要重寫的方法的行為。 因為在試圖修改或重寫已有方法的行為時,很可能會破壞 MyBatis 的核心模組。 這些都是更底層的類和方法,所以使用外掛的時候要特別當心。

通過 MyBatis 提供的強大機制,使用外掛是非常簡單的,只需實現 Interceptor 介面,並指定想要攔截的方法簽名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的外掛將會攔截在 Executor 例項中所有的 “update” 方法呼叫, 這裡的 Executor 是負責執行底層對映語句的內部物件。

提示 覆蓋配置類

除了用外掛來修改 MyBatis 核心行為以外,還可以通過完全覆蓋配置類來達到目的。只需繼承配置類後覆蓋其中的某個方法,再把它傳遞到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,這可能會極大影響 MyBatis 的行為,務請慎之又慎。

環境配置(environments)

MyBatis 可以配置成適應多種環境,這種機制有助於將 SQL 對映應用於多種資料庫之中, 現實情況下有多種理由需要這麼做。例如,開發、測試和生產環境需要有不同的配置;或者想在具有相同 Schema 的多個生產資料庫中使用相同的 SQL 對映。還有許多類似的使用場景。

不過要記住:儘管可以配置多個環境,但每個 SqlSessionFactory 例項只能選擇一種環境。

所以,如果你想連線兩個資料庫,就需要建立兩個 SqlSessionFactory 例項,每個資料庫對應一個。而如果是三個資料庫,就需要三個例項,依此類推,記起來很簡單:

  • 每個資料庫對應一個 SqlSessionFactory 例項

為了指定建立哪種環境,只要將它作為可選的引數傳遞給 SqlSessionFactoryBuilder 即可。可以接受環境配置的兩個方法簽名是:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

如果忽略了環境引數,那麼將會載入預設環境,如下所示:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);

environments 元素定義瞭如何配置環境。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意一些關鍵點:

  • 預設使用的環境 ID(比如:default="development")。
  • 每個 environment 元素定義的環境 ID(比如:id="development")。
  • 事務管理器的配置(比如:type="JDBC")。
  • 資料來源的配置(比如:type="POOLED")。

預設環境和環境 ID 顧名思義。 環境可以隨意命名,但務必保證預設的環境 ID 要匹配其中一個環境 ID。

事務管理器(transactionManager)

在 MyBatis 中有兩種型別的事務管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 這個配置直接使用了 JDBC 的提交和回滾設施,它依賴從資料來源獲得的連線來管理事務作用域。

  • MANAGED – 這個配置幾乎沒做什麼。它從不提交或回滾一個連線,而是讓容器來管理事務的整個生命週期(比如 JEE 應用伺服器的上下文)。 預設情況下它會關閉連線。然而一些容器並不希望連線被關閉,因此需要將 closeConnection 屬性設定為 false 來阻止預設的關閉行為。例如:

    <transactionManager type="MANAGED">
      <property name="closeConnection" value="false"/>
    </transactionManager>
    

提示 如果你正在使用 Spring + MyBatis,則沒有必要配置事務管理器,因為 Spring 模組會使用自帶的管理器來覆蓋前面的配置。

這兩種事務管理器型別都不需要設定任何屬性。它們其實是類型別名,換句話說,你可以用 TransactionFactory 介面實現類的全限定名或類型別名代替它們。

public interface TransactionFactory {
  default void setProperties(Properties props) { // 從 3.5.2 開始,該方法為預設方法
    // 空實現
  }
  Transaction newTransaction(Connection conn);
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

在事務管理器例項化後,所有在 XML 中配置的屬性將會被傳遞給 setProperties() 方法。你的實現還需要建立一個 Transaction 介面的實現類,這個介面也很簡單:

public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
  Integer getTimeout() throws SQLException;
}

使用這兩個介面,你可以完全自定義 MyBatis 對事務的處理。

資料來源(dataSource)

dataSource 元素使用標準的 JDBC 資料來源介面來配置 JDBC 連線物件的資源。

  • 大多數 MyBatis 應用程式會按示例中的例子來配置資料來源。雖然資料來源配置是可選的,但如果要啟用延遲載入特性,就必須配置資料來源。

有三種內建的資料來源型別(也就是 type="[UNPOOLED|POOLED|JNDI]"):

UNPOOLED– 這個資料來源的實現會每次請求時開啟和關閉連線。雖然有點慢,但對那些資料庫連線可用性要求不高的簡單應用程式來說,是一個很好的選擇。 效能表現則依賴於使用的資料庫,對某些資料庫來說,使用連線池並不重要,這個配置就很適合這種情形。UNPOOLED 型別的資料來源僅僅需要配置以下 5 種屬性:

  • driver – 這是 JDBC 驅動的 Java 類全限定名(並不是 JDBC 驅動中可能包含的資料來源類)。
  • url – 這是資料庫的 JDBC URL 地址。
  • username – 登入資料庫的使用者名稱。
  • password – 登入資料庫的密碼。
  • defaultTransactionIsolationLevel – 預設的連線事務隔離級別。
  • defaultNetworkTimeout – 等待資料庫操作完成的預設網路超時時間(單位:毫秒)。檢視 java.sql.Connection#setNetworkTimeout() 的 API 文件以獲取更多資訊。

作為可選項,你也可以傳遞屬性給資料庫驅動。只需在屬性名加上“driver.”字首即可,例如:

  • driver.encoding=UTF8

這將通過 DriverManager.getConnection(url, driverProperties) 方法傳遞值為 UTF8encoding 屬性給資料庫驅動。

POOLED– 這種資料來源的實現利用“池”的概念將 JDBC 連線物件組織起來,避免了建立新的連線例項時所必需的初始化和認證時間。 這種處理方式很流行,能使併發 Web 應用快速響應請求。

除了上述提到 UNPOOLED 下的屬性外,還有更多屬性用來配置 POOLED 的資料來源:

  • poolMaximumActiveConnections – 在任意時間可存在的活動(正在使用)連線數量,預設值:10
  • poolMaximumIdleConnections – 任意時間可能存在的空閒連線數。
  • poolMaximumCheckoutTime – 在被強制返回之前,池中連線被檢出(checked out)時間,預設值:20000 毫秒(即 20 秒)
  • poolTimeToWait – 這是一個底層設定,如果獲取連線花費了相當長的時間,連線池會列印狀態日誌並重新嘗試獲取一個連線(避免在誤配置的情況下一直失敗且不列印日誌),預設值:20000 毫秒(即 20 秒)。
  • poolMaximumLocalBadConnectionTolerance – 這是一個關於壞連線容忍度的底層設定, 作用於每一個嘗試從快取池獲取連線的執行緒。 如果這個執行緒獲取到的是一個壞的連線,那麼這個資料來源允許這個執行緒嘗試重新獲取一個新的連線,但是這個重新嘗試的次數不應該超過 poolMaximumIdleConnectionspoolMaximumLocalBadConnectionTolerance 之和。 預設值:3(新增於 3.4.5)
  • poolPingQuery – 傳送到資料庫的偵測查詢,用來檢驗連線是否正常工作並準備接受請求。預設是“NO PING QUERY SET”,這會導致多數資料庫驅動出錯時返回恰當的錯誤訊息。
  • poolPingEnabled – 是否啟用偵測查詢。若開啟,需要設定 poolPingQuery 屬性為一個可執行的 SQL 語句(最好是一個速度非常快的 SQL 語句),預設值:false。
  • poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的頻率。可以被設定為和資料庫連線超時時間一樣,來避免不必要的偵測,預設值:0(即所有連線每一時刻都被偵測 — 當然僅當 poolPingEnabled 為 true 時適用)。

JNDI – 這個資料來源實現是為了能在如 EJB 或應用伺服器這類容器中使用,容器可以集中或在外部配置資料來源,然後放置一個 JNDI 上下文的資料來源引用。這種資料來源配置只需要兩個屬性:

  • initial_context – 這個屬性用來在 InitialContext 中尋找上下文(即,initialContext.lookup(initial_context))。這是個可選屬性,如果忽略,那麼將會直接從 InitialContext 中尋找 data_source 屬性。
  • data_source – 這是引用資料來源例項位置的上下文路徑。提供了 initial_context 配置時會在其返回的上下文中進行查詢,沒有提供時則直接在 InitialContext 中查詢。

和其他資料來源配置類似,可以通過新增字首“env.”直接把屬性傳遞給 InitialContext。比如:

  • env.encoding=UTF8

這就會在 InitialContext 例項化時往它的構造方法傳遞值為 UTF8encoding 屬性。

你可以通過實現介面 org.apache.ibatis.datasource.DataSourceFactory 來使用第三方資料來源實現:

public interface DataSourceFactory {
  void setProperties(Properties props);
  DataSource getDataSource();
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父類來構建新的資料來源介面卡,比如下面這段插入 C3P0 資料來源所必需的程式碼:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {

  public C3P0DataSourceFactory() {
    this.dataSource = new ComboPooledDataSource();
  }
}

為了令其工作,記得在配置檔案中為每個希望 MyBatis 呼叫的 setter 方法增加對應的屬性。 下面是一個可以連線至 PostgreSQL 資料庫的例子:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
</dataSource>

資料庫廠商標識(databaseIdProvider)

MyBatis 可以根據不同的資料庫廠商執行不同的語句,這種多廠商的支援是基於對映語句中的 databaseId 屬性。 MyBatis 會載入帶有匹配當前資料庫 databaseId 屬性和所有不帶 databaseId 屬性的語句。 如果同時找到帶有 databaseId 和不帶 databaseId 的相同語句,則後者會被捨棄。 為支援多廠商特性,只要像下面這樣在 mybatis-config.xml 檔案中加入 databaseIdProvider 即可:

<databaseIdProvider type="DB_VENDOR" />

databaseIdProvider 對應的 DB_VENDOR 實現會將 databaseId 設定為 DatabaseMetaData#getDatabaseProductName() 返回的字串。 由於通常情況下這些字串都非常長,而且相同產品的不同版本會返回不同的值,你可能想通過設定屬性別名來使其變短:

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

在提供了屬性別名時,databaseIdProvider 的 DB_VENDOR 實現會將 databaseId 設定為資料庫產品名與屬性中的名稱第一個相匹配的值,如果沒有匹配的屬性,將會設定為 “null”。 在這個例子中,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 將被設定為“oracle”。

你可以通過實現介面 org.apache.ibatis.mapping.DatabaseIdProvider 並在 mybatis-config.xml 中註冊來構建自己的 DatabaseIdProvider:

public interface DatabaseIdProvider {
  default void setProperties(Properties p) { // 從 3.5.2 開始,該方法為預設方法
    // 空實現
  }
  String getDatabaseId(DataSource dataSource) throws SQLException;
}

對映器(mappers)

既然 MyBatis 的行為已經由上述元素配置完了,我們現在就要來定義 SQL 對映語句了。 但首先,我們需要告訴 MyBatis 到哪裡去找到這些語句。 在自動查詢資源方面,Java 並沒有提供一個很好的解決方案,所以最好的辦法是直接告訴 MyBatis 到哪裡去找對映檔案。 你可以使用相對於類路徑的資源引用,或完全限定資源定位符(包括 file:/// 形式的 URL),或類名和包名等。例如:

<!-- 使用相對於類路徑的資源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定資源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用對映器介面實現類的完全限定類名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 將包內的對映器介面實現全部註冊為對映器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

這些配置會告訴 MyBatis 去哪裡找對映檔案,剩下的細節就應該是每個 SQL 對映檔案了,也就是接下來我們要討論的。

XML 對映器

MyBatis 的真正強大在於它的語句對映,這是它的魔力所在。由於它的異常強大,對映器的 XML 檔案就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 程式碼進行對比,你會立即發現省掉了將近 95% 的程式碼。MyBatis 致力於減少使用成本,讓使用者能更專注於 SQL 程式碼。

SQL 對映檔案只有很少的幾個頂級元素(按照應被定義的順序列出):

  • cache – 該名稱空間的快取配置。
  • cache-ref – 引用其它名稱空間的快取配置。
  • resultMap – 描述如何從資料庫結果集中載入物件,是最複雜也是最強大的元素。
  • parameterMap – 老式風格的引數對映。此元素已被廢棄,並可能在將來被移除!請使用行內參數對映。文件中不會介紹此元素。
  • sql – 可被其它語句引用的可重用語句塊。
  • insert – 對映插入語句。
  • update – 對映更新語句。
  • delete – 對映刪除語句。
  • select – 對映查詢語句。

下一部分將從語句本身開始來描述每個元素的細節。

select

查詢語句是 MyBatis 中最常用的元素之一——光能把資料存到資料庫中價值並不大,還要能重新取出來才有用,多數應用也都是查詢比修改要頻繁。 MyBatis 的基本原則之一是:在每個插入、更新或刪除操作之間,通常會執行多個查詢操作。因此,MyBatis 在查詢和結果對映做了相當多的改進。一個簡單查詢的 select 元素是非常簡單的。比如:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

這個語句名為 selectPerson,接受一個 int(或 Integer)型別的引數,並返回一個 HashMap 型別的物件,其中的鍵是列名,值便是結果行中的對應值。

注意引數符號:

#{id}

這就告訴 MyBatis 建立一個預處理語句(PreparedStatement)引數,在 JDBC 中,這樣的一個引數在 SQL 中會由一個“?”來標識,並被傳遞到一個新的預處理語句中,就像這樣:

// 近似的 JDBC 程式碼,非 MyBatis 程式碼...
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

當然,使用 JDBC 就意味著使用更多的程式碼,以便提取結果並將它們對映到物件例項中,而這就是 MyBatis 的拿手好戲。引數和結果對映的詳細細節會分別在後面單獨的小節中說明。

select 元素允許你配置很多屬性來配置每條語句的行為細節。

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
屬性 描述
id 在名稱空間中唯一的識別符號,可以被用來引用這條語句。
parameterType 將會傳入這條語句的引數的類全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過型別處理器(TypeHandler)推斷出具體傳入語句的引數,預設值為未設定(unset)。
parameterMap 用於引用外部 parameterMap 的屬性,目前已被廢棄。請使用行內參數對映和 parameterType 屬性。
resultType 期望從這條語句中返回結果的類全限定名或別名。 注意,如果返回的是集合,那應該設定為集合包含的型別,而不是集合本身的型別。 resultType 和 resultMap 之間只能同時使用一個。
resultMap 對外部 resultMap 的命名引用。結果對映是 MyBatis 最強大的特性,如果你對其理解透徹,許多複雜的對映問題都能迎刃而解。 resultType 和 resultMap 之間只能同時使用一個。
flushCache 將其設定為 true 後,只要語句被呼叫,都會導致本地快取和二級快取被清空,預設值:false。
useCache 將其設定為 true 後,將會導致本條語句的結果被二級快取快取起來,預設值:對 select 元素為 true。
timeout 這個設定是在丟擲異常之前,驅動程式等待資料庫返回請求結果的秒數。預設值為未設定(unset)(依賴資料庫驅動)。
fetchSize 這是一個給驅動的建議值,嘗試讓驅動程式每次批量返回的結果行數等於這個設定值。 預設值為未設定(unset)(依賴驅動)。
statementType 可選 STATEMENT,PREPARED 或 CALLABLE。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED。
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等價於 unset) 中的一個,預設值為 unset (依賴資料庫驅動)。
databaseId 如果配置了資料庫廠商標識(databaseIdProvider),MyBatis 會載入所有不帶 databaseId 或匹配當前 databaseId 的語句;如果帶和不帶的語句都有,則不帶的會被忽略。
resultOrdered 這個設定僅針對巢狀結果 select 語句:如果為 true,將會假設包含了巢狀結果集或是分組,當返回一個主結果行時,就不會產生對前面結果集的引用。 這就使得在獲取巢狀結果集的時候不至於記憶體不夠用。預設值:false
resultSets 這個設定僅適用於多結果集的情況。它將列出語句執行後返回的結果集並賦予每個結果集一個名稱,多個名稱之間以逗號分隔。

insert, update 和 delete

資料變更語句 insert,update 和 delete 的實現非常接近:

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
屬性 描述
id 在名稱空間中唯一的識別符號,可以被用來引用這條語句。
parameterType 將會傳入這條語句的引數的類全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過型別處理器(TypeHandler)推斷出具體傳入語句的引數,預設值為未設定(unset)。
parameterMap 用於引用外部 parameterMap 的屬性,目前已被廢棄。請使用行內參數對映和 parameterType 屬性。
flushCache 將其設定為 true 後,只要語句被呼叫,都會導致本地快取和二級快取被清空,預設值:(對 insert、update 和 delete 語句)true。
timeout 這個設定是在丟擲異常之前,驅動程式等待資料庫返回請求結果的秒數。預設值為未設定(unset)(依賴資料庫驅動)。
statementType 可選 STATEMENT,PREPARED 或 CALLABLE。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED。
useGeneratedKeys (僅適用於 insert 和 update)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法來取出由資料庫內部生成的主鍵(比如:像 MySQL 和 SQL Server 這樣的關係型資料庫管理系統的自動遞增欄位),預設值:false。
keyProperty (僅適用於 insert 和 update)指定能夠唯一識別物件的屬性,MyBatis 會使用 getGeneratedKeys 的返回值或 insert 語句的 selectKey 子元素設定它的值,預設值:未設定(unset)。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
keyColumn (僅適用於 insert 和 update)設定生成鍵值在表中的列名,在某些資料庫(像 PostgreSQL)中,當主鍵列不是表中的第一列的時候,是必須設定的。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
databaseId 如果配置了資料庫廠商標識(databaseIdProvider),MyBatis 會載入所有不帶 databaseId 或匹配當前 databaseId 的語句;如果帶和不帶的語句都有,則不帶的會被忽略。

下面是 insert,update 和 delete 語句的示例:

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

如前所述,插入語句的配置規則更加豐富,在插入語句裡面有一些額外的屬性和子元素用來處理主鍵的生成,並且提供了多種生成方式。

首先,如果你的資料庫支援自動生成主鍵的欄位(比如 MySQL 和 SQL Server),那麼你可以設定 useGeneratedKeys=”true”,然後再把 keyProperty 設定為目標屬性就 OK 了。例如,如果上面的 Author 表已經在 id 列上使用了自動生成,那麼語句可以修改為:

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

如果你的資料庫還支援多行插入, 你也可以傳入一個 Author 陣列或集合,並返回自動生成的主鍵。

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

對於不支援自動生成主鍵列的資料庫和可能不支援自動生成主鍵的 JDBC 驅動,MyBatis 有另外一種方法來生成主鍵。

這裡有一個簡單(也很傻)的示例,它可以生成一個隨機 ID(不建議實際使用,這裡只是為了展示 MyBatis 處理問題的靈活性和寬容度):

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

在上面的示例中,首先會執行 selectKey 元素中的語句,並設定 Author 的 id,然後才會呼叫插入語句。這樣就實現了資料庫自動生成主鍵類似的行為,同時保持了 Java 程式碼的簡潔。

selectKey 元素描述如下:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
屬性 描述
keyProperty selectKey 語句結果應該被設定到的目標屬性。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
keyColumn 返回結果集中生成列屬性的列名。如果生成列不止一個,可以用逗號分隔多個屬性名稱。
resultType 結果的型別。通常 MyBatis 可以推斷出來,但是為了更加準確,寫上也不會有什麼問題。MyBatis 允許將任何簡單型別用作主鍵的型別,包括字串。如果生成列不止一個,則可以使用包含期望屬性的 Object 或 Map。
order 可以設定為 BEFOREAFTER。如果設定為 BEFORE,那麼它首先會生成主鍵,設定 keyProperty 再執行插入語句。如果設定為 AFTER,那麼先執行插入語句,然後是 selectKey 中的語句 - 這和 Oracle 資料庫的行為相似,在插入語句內部可能有嵌入索引呼叫。
statementType 和前面一樣,MyBatis 支援 STATEMENTPREPAREDCALLABLE 型別的對映語句,分別代表 Statement, PreparedStatementCallableStatement 型別。

sql

這個元素可以用來定義可重用的 SQL 程式碼片段,以便在其它語句中使用。 引數可以靜態地(在載入的時候)確定下來,並且可以在不同的 include 元素中定義不同的引數值。比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

這個 SQL 片段可以在其它語句中使用,例如:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

也可以在 include 元素的 refid 屬性或內部語句中使用屬性值,例如:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

引數

之前見到的所有語句都使用了簡單的引數形式。但實際上,引數是 MyBatis 非常強大的元素。對於大多數簡單的使用場景,你都不需要使用複雜的引數,比如:

<select id="selectUsers" resultType="User">
  select id, username, password
  from users
  where id = #{id}
</select>

上面的這個示例說明了一個非常簡單的命名引數對映。鑑於引數型別(parameterType)會被自動設定為 int,這個引數可以隨意命名。原始型別或簡單資料型別(比如 IntegerString)因為沒有其它屬性,會用它們的值來作為引數。 然而,如果傳入一個複雜的物件,行為就會有點不一樣了。比如:

<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

如果 User 型別的引數物件傳遞到了語句中,會查詢 id、username 和 password 屬性,然後將它們的值傳入預處理語句的引數中。

對傳遞語句引數來說,這種方式真是乾脆利落。不過引數對映的功能遠不止於此。

首先,和 MyBatis 的其它部分一樣,引數也可以指定一個特殊的資料型別。

#{property,javaType=int,jdbcType=NUMERIC}

和 MyBatis 的其它部分一樣,幾乎總是可以根據引數物件的型別確定 javaType,除非該物件是一個 HashMap。這個時候,你需要顯式指定 javaType 來確保正確的型別處理器(TypeHandler)被使用。

提示 JDBC 要求,如果一個列允許使用 null 值,並且會使用值為 null 的引數,就必須要指定 JDBC 型別(jdbcType)。閱讀 PreparedStatement.setNull()的 JavaDoc 來獲取更多資訊。

要更進一步地自定義型別處理方式,可以指定一個特殊的型別處理器類(或別名),比如:

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

引數的配置好像越來越繁瑣了,但實際上,很少需要如此繁瑣的配置。

對於數值型別,還可以設定 numericScale 指定小數點後保留的位數。

#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

最後,mode 屬性允許你指定 INOUTINOUT 引數。如果引數的 modeOUTINOUT,將會修改引數物件的屬性值,以便作為輸出引數返回。 如果 modeOUT(或 INOUT),而且 jdbcTypeCURSOR(也就是 Oracle 的 REFCURSOR),你必須指定一個 resultMap 引用來將結果集 ResultMap 對映到引數的型別上。要注意這裡的 javaType 屬性是可選的,如果留空並且 jdbcType 是 CURSOR,它會被自動地被設為 ResultMap

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis 也支援很多高階的資料型別,比如結構體(structs),但是當使用 out 引數時,你必須顯式設定型別的名稱。比如(再次提示,在實際中要像這樣不能換行):

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

儘管上面這些選項很強大,但大多時候,你只須簡單指定屬性名,頂多要為可能為空的列指定 jdbcType,其他的事情交給 MyBatis 自己去推斷就行了。

#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}

字串替換

預設情況下,使用 #{} 引數語法時,MyBatis 會建立 PreparedStatement 引數佔位符,並通過佔位符安全地設定引數(就像使用 ? 一樣)。 這樣做更安全,更迅速,通常也是首選做法,不過有時你就是想直接在 SQL 語句中直接插入一個不轉義的字串。 比如 ORDER BY 子句,這時候你可以:

ORDER BY ${columnName}

這樣,MyBatis 就不會修改或轉義該字串了。

當 SQL 語句中的元資料(如表名或列名)是動態生成的時候,字串替換將會非常有用。 舉個例子,如果你想 select 一個表任意一列的資料時,不需要這樣寫:

@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

// 其它的 "findByXxx" 方法

而是可以只寫這樣一個方法:

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

其中 ${column} 會被直接替換,而 #{value} 會使用 ? 預處理。 這樣,就能完成同樣的任務:

User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "[email protected]");

這種方式也同樣適用於替換表名的情況。

提示 用這種方式接受使用者的輸入,並用作語句引數是不安全的,會導致潛在的 SQL 注入攻擊。因此,要麼不允許使用者輸入這些欄位,要麼自行轉義並檢驗這些引數。

結果對映

resultMap 元素是 MyBatis 中最重要最強大的元素。它可以讓你從 90% 的 JDBC ResultSets 資料提取程式碼中解放出來,並在一些情形下允許你進行一些 JDBC 不支援的操作。實際上,在為一些比如連線的複雜語句編寫對映程式碼的時候,一份 resultMap 能夠代替實現同等功能的數千行程式碼。ResultMap 的設計思想是,對簡單的語句做到零配置,對於複雜一點的語句,只需要描述語句之間的關係就行了。

之前你已經見過簡單對映語句的示例,它們沒有顯式指定 resultMap。比如:

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

上述語句只是簡單地將所有的列對映到 HashMap 的鍵上,這由 resultType 屬性指定。雖然在大部分情況下都夠用,但是 HashMap 並不是一個很好的領域模型。你的程式更可能會使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 物件)作為領域模型。MyBatis 對兩者都提供了支援。看看下面這個 JavaBean:

package com.someapp.model;
public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}

基於 JavaBean 的規範,上面這個類有 3 個屬性:id,username 和 hashedPassword。這些屬性會對應到 select 語句中的列名。

這樣的一個 JavaBean 可以被對映到 ResultSet,就像對映到 HashMap 一樣簡單。

<select id="selectUsers" resultType="com.someapp.model.User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

類型別名是你的好幫手。使用它們,你就可以不用輸入類的全限定名了。比如:

<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- SQL 對映 XML 中 -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

在這些情況下,MyBatis 會在幕後自動建立一個 ResultMap,再根據屬性名來對映列到 JavaBean 的屬性上。如果列名和屬性名不能匹配上,可以在 SELECT 語句中設定列別名(這是一個基本的 SQL 特性)來完成匹配。比如:

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

在學習了上面的知識後,你會發現上面的例子沒有一個需要顯式配置 ResultMap,這就是 ResultMap 的優秀之處——你完全可以不用顯式地配置它們。 雖然上面的例子不用顯式配置 ResultMap。 但為了講解,我們來看看如果在剛剛的示例中,顯式使用外部的 resultMap 會怎樣,這也是解決列名不匹配的另外一種方式。

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

然後在引用它的語句中設定 resultMap 屬性就行了(注意我們去掉了 resultType 屬性)。比如:

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>

如果這個世界總是這麼簡單就好了。

高階結果對映

MyBatis 建立時的一個思想是:資料庫不可能永遠是你所想或所需的那個樣子。 我們希望每個資料庫都具備良好的第三正規化或 BCNF 正規化,可惜它們並不都是那樣。 如果能有一種資料庫對映模式,完美適配所有的應用程式,那就太好了,但可惜也沒有。 而 ResultMap 就是 MyBatis 對這個問題的答案。

比如,我們如何對映下面這個語句?

<!-- 非常複雜的語句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  select
       B.id as blog_id,
       B.title as blog_title,
       B.author_id as blog_author_id,
       A.id as author_id,
       A.username as author_username,
       A.password as author_password,
       A.email as author_email,
       A.bio as author_bio,
       A.favourite_section as author_favourite_section,
       P.id as post_id,
       P.blog_id as post_blog_id,
       P.author_id as post_author_id,
       P.created_on as post_created_on,
       P.section as post_section,
       P.subject as post_subject,
       P.draft as draft,
       P.body as post_body,
       C.id as comment_id,
       C.post_id as comment_post_id,
       C.name as comment_name,
       C.comment as comment_text,
       T.id as tag_id,
       T.name as tag_name
  from Blog B
       left outer join Author A on B.author_id = A.id
       left outer join Post P on B.id = P.blog_id
       left outer join Comment C on P.id = C.post_id
       left outer join Post_Tag PT on PT.post_id = P.id
       left outer join Tag T on PT.tag_id = T.id
  where B.id = #{id}
</select>

你可能想把它對映到一個智慧的物件模型,這個物件表示了一篇部落格,它由某位作者所寫,有很多的博文,每篇博文有零或多條的評論和標籤。 我們先來看看下面這個完整的例子,它是一個非常複雜的結果對映(假設作者,部落格,博文,評論和標籤都是類型別名)。 不用緊張,我們會一步一步地來說明。雖然它看起來令人望而生畏,但其實非常簡單。

<!-- 非常複雜的結果對映 -->
<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

resultMap 元素有很多子元素和一個值得深入探討的結構。 下面是resultMap 元素的概念檢視。

結果對映(resultMap)

  • constructor
    

    - 用於在例項化類時,注入結果到構造方法中

    • idArg - ID 引數;標記出作為 ID 的結果可以幫助提高整體效能
    • arg - 將被注入到構造方法的一個普通結果
  • id – 一個 ID 結果;標記出作為 ID 的結果可以幫助提高整體效能

  • result – 注入到欄位或 JavaBean 屬性的普通結果

  • association
    

    – 一個複雜型別的關聯;許多結果將包裝成這種型別

    • 巢狀結果對映 – 關聯可以是 resultMap 元素,或是對其它結果對映的引用
  • collection
    

    – 一個複雜型別的集合

    • 巢狀結果對映 – 集合可以是 resultMap 元素,或是對其它結果對映的引用
  • discriminator
    

    – 使用結果值來決定使用哪個

    resultMap
    
    • case
      

      – 基於某些值的結果對映

      • 巢狀結果對映 – case 也是一個結果對映,因此具有相同的結構和元素;或者引用其它的結果對映
屬性 描述
id 當前名稱空間中的一個唯一標識,用於標識一個結果對映。
type 類的完全限定名, 或者一個類型別名(關於內建的類型別名,可以參考上面的表格)。
autoMapping 如果設定這個屬性,MyBatis 將會為本結果對映開啟或者關閉自動對映。 這個屬性會覆蓋全域性的屬性 autoMappingBehavior。預設值:未設定(unset)。

最佳實踐 最好逐步建立結果對映。單元測試可以在這個過程中起到很大幫助。 如果你嘗試一次性建立像上面示例那麼巨大的結果對映,不僅容易出錯,難度也會直線上升。 所以,從最簡單的形態開始,逐步迭代。而且別忘了單元測試! 有時候,框架的行為像是一個黑盒子(無論是否開源)。因此,為了確保實現的行為與你的期望相一致,最好編寫單元測試。 並且單元測試在提交 bug 時也能起到很大的作用。

下一部分將詳細說明每個元素。

id & result

<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

這些元素是結果對映的基礎。idresult 元素都將一個列的值對映到一個簡單資料型別(String, int, double, Date 等)的屬性或欄位。

這兩者之間的唯一不同是,id 元素對應的屬性會被標記為物件的識別符號,在比較物件例項時使用。 這樣可以提高整體的效能,尤其是進行快取和巢狀結果對映(也就是連線對映)的時候。

兩個元素都有一些屬性:

屬性 描述
property 對映到列結果的欄位或屬性。如果 JavaBean 有這個名字的屬性(property),會先使用該屬性。否則 MyBatis 將會尋找給定名稱的欄位(field)。 無論是哪一種情形,你都可以使用常見的點式分隔形式進行復雜屬性導航。 比如,你可以這樣對映一些簡單的東西:“username”,或者對映到一些複雜的東西上:“address.street.number”。
column 資料庫中的列名,或者是列的別名。一般情況下,這和傳遞給 resultSet.getString(columnName) 方法的引數一樣。
javaType 一個 Java 類的全限定名,或一個類型別名(關於內建的類型別名,可以參考上面的表格)。 如果你對映到一個 JavaBean,MyBatis 通常可以推斷型別。然而,如果你對映到的是 HashMap,那麼你應該明確地指定 javaType 來保證行為與期望的相一致。
jdbcType JDBC 型別,所支援的 JDBC 型別參見這個表格之後的“支援的 JDBC 型別”。 只需要在可能執行插入、更新和刪除的且允許空值的列上指定 JDBC 型別。這是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 程式設計,你需要對可以為空值的列指定這個型別。
typeHandler 我們在前面討論過預設的型別處理器。使用這個屬性,你可以覆蓋預設的型別處理器。 這個屬性值是一個型別處理器實現類的全限定名,或者是類型別名。

支援的 JDBC 型別

為了以後可能的使用場景,MyBatis 通過內建的 jdbcType 列舉型別支援下面的 JDBC 型別。

BIT FLOAT CHAR TIMESTAMP OTHER UNDEFINED
TINYINT REAL VARCHAR BINARY BLOB NVARCHAR
SMALLINT DOUBLE LONGVARCHAR VARBINARY CLOB NCHAR
INTEGER NUMERIC DATE LONGVARBINARY BOOLEAN NCLOB
BIGINT DECIMAL TIME NULL CURSOR ARRAY

構造方法

通過修改物件屬性的方式,可以滿足大多數的資料傳輸物件(Data Transfer Object, DTO)以及絕大部分領域模型的要求。但有些情況下你想使用不可變類。 一般來說,很少改變或基本不變的包含引用或資料的表,很適合使用不可變類。 構造方法注入允許你在初始化時為類設定屬性的值,而不用暴露出公有方法。MyBatis 也支援私有屬性和私有 JavaBean 屬性來完成注入,但有一些人更青睞於通過構造方法進行注入。 constructor 元素就是為此而生的。

看看下面這個構造方法:

public class User {
   //...
   public User(Integer id, String username, int age) {
     //...
  }
//...
}

為了將結果注入構造方法,MyBatis 需要通過某種方式定位相應的構造方法。 在下面的例子中,MyBatis 搜尋一個聲明瞭三個形參的構造方法,引數型別以 java.lang.Integer, java.lang.Stringint 的順序給出。

<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

當你在處理一個帶有多個形參的構造方法時,很容易搞亂 arg 元素的順序。 從版本 3.4.3 開始,可以在指定引數名稱的前提下,以任意順序編寫 arg 元素。 為了通過名稱來引用構造方法引數,你可以新增 @Param 註解,或者使用 '-parameters' 編譯選項並啟用 useActualParamName 選項(預設開啟)來編譯專案。下面是一個等價的例子,儘管函式簽名中第二和第三個形參的順序與 constructor 元素中引數宣告的順序不匹配。

<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

如果存在名稱和型別相同的屬性,那麼可以省略 javaType

剩餘的屬性和規則和普通的 id 和 result 元素是一樣的。

屬性 描述
column 資料庫中的列名,或者是列的別名。一般情況下,這和傳遞給 resultSet.getString(columnName) 方法的引數一樣。
javaType 一個 Java 類的完全限定名,或一個類型別名(關於內建的類型別名,可以參考上面的表格)。 如果你對映到一個 JavaBean,MyBatis 通常可以推斷型別。然而,如果你對映到的是 HashMap,那麼你應該明確地指定 javaType 來保證行為與期望的相一致。
jdbcType JDBC 型別,所支援的 JDBC 型別參見這個表格之前的“支援的 JDBC 型別”。 只需要在可能執行插入、更新和刪除的且允許空值的列上指定 JDBC 型別。這是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 程式設計,你需要對可能存在空值的列指定這個型別。
typeHandler 我們在前面討論過預設的型別處理器。使用這個屬性,你可以覆蓋預設的型別處理器。 這個屬性值是一個型別處理器實現類的完全限定名,或者是類型別名。
select 用於載入複雜型別屬性的對映語句的 ID,它會從 column 屬性中指定的列檢索資料,作為引數傳遞給此 select 語句。具體請參考關聯元素。
resultMap 結果對映的 ID,可以將巢狀的結果集對映到一個合適的物件樹中。 它可以作為使用額外 select 語句的替代方案。它可以將多表連線操作的結果對映成一個單一的 ResultSet。這樣的 ResultSet 將會將包含重複或部分資料重複的結果集。為了將結果集正確地對映到巢狀的物件樹中,MyBatis 允許你 “串聯”結果對映,以便解決巢狀結果集的問題。想了解更多內容,請參考下面的關聯元素。
name 構造方法形參的名字。從 3.4.3 版本開始,通過指定具體的引數名,你可以以任意順序寫入 arg 元素。參看上面的解釋。

關聯

<association property="author" column="blog_author_id" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
</association>

關聯(association)元素處理“有一個”型別的關係。 比如,在我們的示例中,一個部落格有一個使用者。關聯結果對映和其它型別的對映工作方式差不多。 你需要指定目標屬性名以及屬性的javaType(很多時候 MyBatis 可以自己推斷出來),在必要的情況下你還可以設定 JDBC 型別,如果你想覆蓋獲取結果值的過程,還可以設定型別處理器。

關聯的不同之處是,你需要告訴 MyBatis 如何載入關聯。MyBatis 有兩種不同的方式載入關聯:

  • 巢狀 Select 查詢:通過執行另外一個 SQL 對映語句來載入期望的複雜型別。
  • 巢狀結果對映:使用巢狀的結果對映來處理連線結果的重複子集。

首先,先讓我們來看看這個元素的屬性。你將會發現,和普通的結果對映相比,它只在 select 和 resultMap 屬性上有所不同。

屬性 描述
property 對映到列結果的欄位或屬性。如果用來匹配的 JavaBean 存在給定名字的屬性,那麼它將會被使用。否則 MyBatis 將會尋找給定名稱的欄位。 無論是哪一種情形,你都可以使用通常的點式分隔形式進行復雜屬性導航。 比如,你可以這樣對映一些簡單的東西:“username”,或者對映到一些複雜的東西上:“address.street.number”。
javaType 一個 Java 類的完全限定名,或一個類型別名(關於內建的類型別名,可以參考上面的表格)。 如果你對映到一個 JavaBean,MyBatis 通常可以推斷型別。然而,如果你對映到的是 HashMap,那麼你應該明確地指定 javaType 來保證行為與期望的相一致。
jdbcType JDBC 型別,所支援的 JDBC 型別參見這個表格之前的“支援的 JDBC 型別”。 只需要在可能執行插入、更新和刪除的且允許空值的列上指定 JDBC 型別。這是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 程式設計,你需要對可能存在空值的列指定這個型別。
typeHandler 我們在前面討論過預設的型別處理器。使用這個屬性,你可以覆蓋預設的型別處理器。 這個屬性值是一個型別處理器實現類的完全限定名,或者是類型別名。

關聯的巢狀 Select 查詢

屬性 描述
column 資料庫中的列名,或者是列的別名。一般情況下,這和傳遞給 resultSet.getString(columnName) 方法的引數一樣。 注意:在使用複合主鍵的時候,你可以使用 column="{prop1=col1,prop2=col2}" 這樣的語法來指定多個傳遞給巢狀 Select 查詢語句的列名。這會使得 prop1prop2 作為引數物件,被設定為對應巢狀 Select 語句的引數。
select 用於載入複雜型別屬性的對映語句的 ID,它會從 column 屬性指定的列中檢索資料,作為引數傳遞給目標 select 語句。 具體請參考下面的例子。注意:在使用複合主鍵的時候,你可以使用 column="{prop1=col1,prop2=col2}" 這樣的語法來指定多個傳遞給巢狀 Select 查詢語句的列名。這會使得 prop1prop2 作為引數物件,被設定為對應巢狀 Select 語句的引數。
fetchType 可選的。有效值為 lazyeager。 指定屬性後,將在對映中忽略全域性配置引數 lazyLoadingEnabled,使用屬性的值。

示例:

<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

就是這麼簡單。我們有兩個 select 查詢語句:一個用來載入部落格(Blog),另外一個用來載入作者(Author),而且部落格的結果對映描述了應該使用 selectAuthor 語句載入它的 author 屬性。

其它所有的屬性將會被自動載入,只要它們的列名和屬性名相匹配。

這種方式雖然很簡單,但在大型資料集或大型資料表上表現不佳。這個問題被稱為“N+1 查詢問題”。 概括地講,N+1 查詢問題是這樣子的:

  • 你執行了一個單獨的 SQL 語句來獲取結果的一個列表(就是“+1”)。
  • 對列表返回的每條記錄,你執行一個 select 查詢語句來為每條記錄載入詳細資訊(就是“N”)。

這個問題會導致成百上千的 SQL 語句被執行。有時候,我們不希望產生這樣的後果。

好訊息是,MyBatis 能夠對這樣的查詢進行延遲載入,因此可以將大量語句同時執行的開銷分散開來。 然而,如果你載入記錄列表之後立刻就遍歷列表以獲取巢狀的資料,就會觸發所有的延遲載入查詢,效能可能會變得很糟糕。

所以還有另外一種方法。

關聯的巢狀結果對映

屬性 描述
resultMap 結果對映的 ID,可以將此關聯的巢狀結果集對映到一個合適的物件樹中。 它可以作為使用額外 select 語句的替代方案。它可以將多表連線操作的結果對映成一個單一的 ResultSet。這樣的 ResultSet 有部分資料是重複的。 為了將結果集正確地對映到巢狀的物件樹中, MyBatis 允許你“串聯”結果對映,以便解決巢狀結果集的問題。使用巢狀結果對映的一個例子在表格以後。
columnPrefix 當連線多個表時,你可能會不得不使用列別名來避免在 ResultSet 中產生重複的列名。指定 columnPrefix 列名字首允許你將帶有這些字首的列對映到一個外部的結果對映中。 詳細說明請參考後面的例子。
notNullColumn 預設情況下,在至少一個被對映到屬性的列不為空時,子物件才會被建立。 你可以在這個屬性上指定非空的列來改變預設行為,指定後,Mybatis 將只在這些列非空時才建立一個子物件。可以使用逗號分隔來指定多個列。預設值:未設定(unset)。
autoMapping 如果設定這個屬性,MyBatis 將會為本結果對映開啟或者關閉自動對映。 這個屬性會覆蓋全域性的屬性 autoMappingBehavior。注意,本屬性對外部的結果對映無效,所以不能搭配 selectresultMap 元素使用。預設值:未設定(unset)。

之前,你已經看到了一個非常複雜的巢狀關聯的例子。 下面的例子則是一個非常簡單的例子,用於演示巢狀結果對映如何工作。 現在我們將部落格表和作者表連線在一起,而不是執行一個獨立的查詢語句,就像這樣:

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

注意查詢中的連線,以及為確保結果能夠擁有唯一且清晰的名字,我們設定的別名。 這使得進行對映非常簡單。現在我們可以對映這個結果:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

在上面的例子中,你可以看到,部落格(Blog)作者(author)的關聯元素委託名為 “authorResult” 的結果對映來載入作者物件的例項。

非常重要: id 元素在巢狀結果對映中扮演著非常重要的角色。你應該總是指定一個或多個可以唯一標識結果的屬性。 雖然,即使不指定這個屬性,MyBatis 仍然可以工作,但是會產生嚴重的效能問題。 只需要指定可以唯一標識結果的最少屬性。顯然,你可以選擇主鍵(複合主鍵也可以)。

現在,上面的示例使用了外部的結果對映元素來對映關聯。這使得 Author 的結果對映可以被重用。 然而,如果你不打算重用它,或者你更喜歡將你所有的結果對映放在一個具有描述性的結果對映元素中。 你可以直接將結果對映作為子元素巢狀在內。這裡給出使用這種方式的等效例子:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
  </association>
</resultMap>

那如果部落格(blog)有一個共同作者(co-author)該怎麼辦?select 語句看起來會是這樣的:

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio,
    CA.id           as co_author_id,
    CA.username     as co_author_username,
    CA.password     as co_author_password,
    CA.email        as co_author_email,
    CA.bio          as co_author_bio
  from Blog B
  left outer join Author A on B.author_id = A.id
  left outer join Author CA on B.co_author_id = CA.id
  where B.id = #{id}
</select>

回憶一下,Author 的結果對映定義如下:

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

由於結果中的列名與結果對映中的列名不同。你需要指定 columnPrefix 以便重複使用該結果對映來對映 co-author 的結果。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author"
    resultMap="authorResult" />
  <association property="coAuthor"
    resultMap="authorResult"
    columnPrefix="co_" />
</resultMap>

關聯的多結果集(ResultSet)

屬性 描述
column 當使用多個結果集時,該屬性指定結果集中用於與 foreignColumn 匹配的列(多個列名以逗號隔開),以識別關係中的父型別與子型別。
foreignColumn 指定外來鍵對應的列名,指定的列將與父型別中 column 的給出的列進行匹配。
resultSet 指定用於載入複雜型別的結果集名字。

從版本 3.2.3 開始,MyBatis 提供了另一種解決 N+1 查詢問題的方法。

某些資料庫允許儲存過程返回多個結果集,或一次性執行多個語句,每個語句返回一個結果集。 我們可以利用這個特性,在不使用連線的情況下,只訪問資料庫一次就能獲得相關資料。

在例子中,儲存過程執行下面的查詢並返回兩個結果集。第一個結果集會返回部落格(Blog)的結果,第二個則返回作者(Author)的結果。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM AUTHOR WHERE ID = #{id}

在對映語句中,必須通過 resultSets 屬性為每個結果集指定一個名字,多個名字使用逗號隔開。

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

現在我們可以指定使用 “authors” 結果集的資料來填充 “author” 關聯:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>

你已經在上面看到了如何處理“有一個”型別的關聯。但是該怎麼處理“有很多個”型別的關聯呢?這就是我們接下來要介紹的。

集合

<collection property="posts" ofType="domain.blog.Post">
  <id property="id" column="post_id"/>
  <result property="subject" column="post_subject"/>
  <result property="body" column="post_body"/>
</collection>

集合元素和關聯元素幾乎是一樣的,它們相似的程度之高,以致於沒有必要再介紹集合元素的相似部分。 所以讓我們來關注它們的不同之處吧。

我們來繼續上面的示例,一個部落格(Blog)只有一個作者(Author)。但一個部落格有很多文章(Post)。 在部落格類中,這可以用下面的寫法來表示:

private List<Post> posts;

要像上面這樣,對映巢狀結果集合到一個 List 中,可以使用集合元素。 和關聯元素一樣,我們可以使用巢狀 Select 查詢,或基於連線的巢狀結果對映集合。

集合的巢狀 Select 查詢

首先,讓我們看看如何使用巢狀 Select 查詢來為部落格載入文章。

<resultMap id="blogResult" type="Blog">
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

你可能會立刻注意到幾個不同,但大部分都和我們上面學習過的關聯元素非常相似。 首先,你會注意到我們使用的是集合元素。 接下來你會注意到有一個新的 “ofType” 屬性。這個屬性非常重要,它用來將 JavaBean(或欄位)屬性的型別和集合儲存的型別區分開來。 所以你可以按照下面這樣來閱讀對映:

<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>

讀作: “posts 是一個儲存 Post 的 ArrayList 集合”

在一般情況下,MyBatis 可以推斷 javaType 屬性,因此並不需要填寫。所以很多時候你可以簡略成:

<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>

集合的巢狀結果對映

現在你可能已經猜到了集合的巢狀結果對映是怎樣工作的——除了新增的 “ofType” 屬性,它和關聯的完全相同。

首先, 讓我們看看對應的 SQL 語句:

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>

我們再次連線了部落格表和文章表,並且為每一列都賦予了一個有意義的別名,以便對映保持簡單。 要對映部落格裡面的文章集合,就這麼簡單:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

再提醒一次,要記得上面 id 元素的重要性,如果你不記得了,請閱讀關聯部分的相關部分。

如果你喜歡更詳略的、可重用的結果對映,你可以使用下面的等價形式:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

集合的多結果集(ResultSet)

像關聯元素那樣,我們可以通過執行儲存過程實現,它會執行兩個查詢並返回兩個結果集,一個是部落格的結果集,另一個是文章的結果集:

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM POST WHERE BLOG_ID = #{id}

在對映語句中,必須通過 resultSets 屬性為每個結果集指定一個名字,多個名字使用逗號隔開。

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>

我們指定 “posts” 集合將會使用儲存在 “posts” 結果集中的資料進行填充:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
    <id property="id" column="id"/>
    <result property="subject" column="subject"/>
    <result property="body" column="body"/>
  </collection>
</resultMap>

注意 對關聯或集合的對映,並沒有深度、廣度或組合上的要求。但在對映時要留意效能問題。 在探索最佳實踐的過程中,應用的單元測試和效能測試會是你的好幫手。 而 MyBatis 的好處在於,可以在不對你的程式碼引入重大變更(如果有)的情況下,允許你之後改變你的想法。

高階關聯和集合對映是一個深度話題。文件的介紹只能到此為止。配合少許的實踐,你會很快了解全部的用法。

鑑別器

<discriminator javaType="int" column="draft">
  <case value="1" resultType="DraftPost"/>
</discriminator>

有時候,一個數據庫查詢可能會返回多個不同的結果集(但總體上還是有一定的聯絡的)。 鑑別器(discriminator)元素就是被設計來應對這種情況的,另外也能處理其它情況,例如類的繼承層次結構。 鑑別器的概念很好理解——它很像 Java 語言中的 switch 語句。

一個鑑別器的定義需要指定 column 和 javaType 屬性。column 指定了 MyBatis 查詢被比較值的地方。 而 javaType 用來確保使用正確的相等測試(雖然很多情況下字串的相等測試都可以工作)。例如:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
</resultMap>

在這個示例中,MyBatis 會從結果集中得到每條記錄,然後比較它的 vehicle type 值。 如果它匹配任意一個鑑別器的 case,就會使用這個 case 指定的結果對映。 這個過程是互斥的,也就是說,剩餘的結果對映將被忽略(除非它是擴充套件的,我們將在稍後討論它)。 如果不能匹配任何一個 case,MyBatis 就只會使用鑑別器塊外定義的結果對映。 所以,如果 carResult 的宣告如下:

<resultMap id="carResult" type="Car">
  <result property="doorCount" column="door_count" />
</resultMap>

那麼只有 doorCount 屬性會被載入。這是為了即使鑑別器的 case 之間都能分為完全獨立的一組,儘管和父結果對映可能沒有什麼關係。在上面的例子中,我們當然知道 cars 和 vehicles 之間有關係,也就是 Car 是一個 Vehicle。因此,我們希望剩餘的屬性也能被載入。而這隻需要一個小修改。

<resultMap id="carResult" type="Car" extends="vehicleResult">
  <result property="doorCount" column="door_count" />
</resultMap>

現在 vehicleResult 和 carResult 的屬性都會被載入了。

可能有人又會覺得對映的外部定義有點太冗長了。 因此,對於那些更喜歡簡潔的對映風格的人來說,還有另一種語法可以選擇。例如:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

提示 請注意,這些都是結果對映,如果你完全不設定任何的 result 元素,MyBatis 將為你自動匹配列和屬性。所以上面的例子大多都要比實際的更復雜。 這也表明,大多數資料庫的複雜度都比較高,我們不太可能一直依賴於這種機制。

自動對映

正如你在前面一節看到的,在簡單的場景下,MyBatis 可以為你自動對映查詢結果。但如果遇到複雜的場景,你需要構建一個結果對映。 但是在本節中,你將看到,你可以混合使用這兩種策略。讓我們深入瞭解一下自動對映是怎樣工作的。

當自動對映查詢結果時,MyBatis 會獲取結果中返回的列名並在 Java 類中查詢相同名字的屬性(忽略大小寫)。 這意味著如果發現了 ID 列和 id 屬性,MyBatis 會將列 ID 的值賦給 id 屬性。

通常資料庫列使用大寫字母組成的單詞命名,單詞間用下劃線分隔;而 Java 屬性一般遵循駝峰命名法約定。為了在這兩種命名方式之間啟用自動對映,需要將 mapUnderscoreToCamelCase 設定為 true。

甚至在提供了結果對映後,自動對映也能工作。在這種情況下,對於每一個結果對映,在 ResultSet 出現的列,如果沒有設定手動對映,將被自動對映。在自動對映處理完畢後,再處理手動對映。 在下面的例子中,iduserName 列將被自動對映,hashed_password 列將根據配置進行對映。

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>
<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>

有三種自動對映等級:

  • NONE - 禁用自動對映。僅對手動對映的屬性進行對映。
  • PARTIAL - 對除在內部定義了巢狀結果對映(也就是連線的屬性)以外的屬性進行對映
  • FULL - 自動對映所有屬性。

預設值是 PARTIAL,這是有原因的。當對連線查詢的結果使用 FULL 時,連線查詢會在同一行中獲取多個不同實體的資料,因此可能導致非預期的對映。 下面的例子將展示這種風險:

<select id="selectBlog" resultMap="blogResult">
  select
    B.id,
    B.title,
    A.username,
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
  <association property="author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <result property="username" column="author_username"/>
</resultMap>

在該結果對映中,BlogAuthor 均將被自動對映。但是注意 Author 有一個 id 屬性,在 ResultSet 中也有一個名為 id 的列,所以 Author 的 id 將填入 Blog 的 id,這可不是你期望的行為。 所以,要謹慎使用 FULL

無論設定的自動對映等級是哪種,你都可以通過在結果對映上設定 autoMapping 屬性來為指定的結果對映設定啟用/禁用自動對映。

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

快取

MyBatis 內建了一個強大的事務性查詢快取機制,它可以非常方便地配置和定製。 為了使它更加強大而且易於配置,我們對 MyBatis 3 中的快取實現進行了許多改進。

預設情況下,只啟用了本地的會話快取,它僅僅對一個會話中的資料進行快取。 要啟用全域性的二級快取,只需要在你的 SQL 對映檔案中新增一行:

<cache/>

基本上就是這樣。這個簡單語句的效果如下:

  • 對映語句檔案中的所有 select 語句的結果將會被快取。
  • 對映語句檔案中的所有 insert、update 和 delete 語句會重新整理快取。
  • 快取會使用最近最少使用演算法(LRU, Least Recently Used)演算法來清除不需要的快取。
  • 快取不會定時進行重新整理(也就是說,沒有重新整理間隔)。
  • 快取會儲存列表或物件(無論查詢方法返回哪種)的 1024 個引用。
  • 快取會被視為讀/寫快取,這意味著獲取到的物件並不是共享的,可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。

提示 快取只作用於 cache 標籤所在的對映檔案中的語句。如果你混合使用 Java API 和 XML 對映檔案,在共用介面中的語句將不會被預設快取。你需要使用 @CacheNamespaceRef 註解指定快取作用域。

這些屬性可以通過 cache 元素的屬性來修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

這個更高階的配置建立了一個 FIFO 快取,每隔 60 秒重新整理,最多可以儲存結果物件或列表的 512 個引用,而且返回的物件被認為是隻讀的,因此對它們進行修改可能會在不同執行緒中的呼叫者產生衝突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最長時間不被使用的物件。
  • FIFO – 先進先出:按物件進入快取的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除物件。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除物件。

預設的清除策略是 LRU。

flushInterval(重新整理間隔)屬性可以被設定為任意的正整數,設定的值應該是一個以毫秒為單位的合理時間量。 預設情況是不設定,也就是沒有重新整理間隔,快取僅僅會在呼叫語句時重新整理。

size(引用數目)屬性可以被設定為任意正整數,要注意欲快取物件的大小和執行環境中可用的記憶體資源。預設值是 1024。

readOnly(只讀)屬性可以被設定為 true 或 false。只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此這些物件不能被修改。這就提供了可觀的效能提升。而可讀寫的快取會(通過序列化)返回快取物件的拷貝。 速度上會慢一些,但是更安全,因此預設值是 false。

提示 二級快取是事務性的。這意味著,當 SqlSession 完成並提交時,或是完成並回滾,但沒有執行 flushCache=true 的 insert/delete/update 語句時,快取會獲得更新。

使用自定義快取

除了上述自定義快取的方式,你也可以通過實現你自己的快取,或為其他第三方快取方案建立介面卡,來完全覆蓋快取行為。

<cache type="com.domain.something.MyCustomCache"/>

這個示例展示瞭如何使用一個自定義的快取實現。type 屬性指定的類必須實現 org.apache.ibatis.cache.Cache 介面,且提供一個接受 String 引數作為 id 的構造器。 這個介面是 MyBatis 框架中許多複雜的介面之一,但是行為卻非常簡單。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

為了對你的快取進行配置,只需要簡單地在你的快取實現中新增公有的 JavaBean 屬性,然後通過 cache 元素傳遞屬性值,例如,下面的例子將在你的快取實現上呼叫一個名為 setCacheFile(String file) 的方法:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

你可以使用所有簡單型別作為 JavaBean 屬性的型別,MyBatis 會進行轉換。 你也可以使用佔位符(如 ${cache.file}),以便替換成在配置檔案屬性中定義的值。

從版本 3.4.2 開始,MyBatis 已經支援在所有屬性設定完畢之後,呼叫一個初始化方法。 如果想要使用這個特性,請在你的自定義快取類裡實現 org.apache.ibatis.builder.InitializingObject 介面。

public interface InitializingObject {
  void initialize() throws Exception;
}

提示 上一節中對快取的配置(如清除策略、可讀或可讀寫等),不能應用於自定義快取。

請注意,快取的配置和快取例項會被繫結到 SQL 對映檔案的名稱空間中。 因此,同一名稱空間中的所有語句和快取將通過名稱空間繫結在一起。 每條語句可以自定義與快取互動的方式,或將它們完全排除於快取之外,這可以通過在每條語句上使用兩個簡單屬性來達成。 預設情況下,語句會這樣來配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

鑑於這是預設行為,顯然你永遠不應該以這樣的方式顯式配置一條語句。但如果你想改變預設的行為,只需要設定 flushCache 和 useCache 屬性。比如,某些情況下你可能希望特定 select 語句的結果排除於快取之外,或希望一條 select 語句清空快取。類似地,你可能希望某些 update 語句執行時不要重新整理快取。

cache-ref

回想一下上一節的內容,對某一名稱空間的語句,只會使用該名稱空間的快取進行快取或重新整理。 但你可能會想要在多個名稱空間中共享相同的快取配置和例項。要實現這種需求,你可以使用 cache-ref 元素來引用另一個快取。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

動態 SQL

動態 SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記新增必要的空格,還要注意去掉列表最後一個列名的逗號。利用動態 SQL,可以徹底擺脫這種痛苦。

使用動態 SQL 並非一件易事,但藉助可用於任何 SQL 對映語句中的強大的動態 SQL 語言,MyBatis 顯著地提升了這一特性的易用性。

如果你之前用過 JSTL 或任何基於類 XML 語言的文字處理器,你對動態 SQL 元素可能會感覺似曾相識。在 MyBatis 之前的版本中,需要花時間瞭解大量的元素。藉助功能強大的基於 OGNL 的表示式,MyBatis 3 替換了之前的大部分元素,大大精簡了元素種類,現在要學習的元素種類比原來的一半還要少。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if

使用動態 SQL 最常見情景是根據條件包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

這條語句提供了可選的查詢文字功能。如果不傳入 “title”,那麼所有處於 “ACTIVE” 狀態的 BLOG 都會返回;如果傳入了 “title” 引數,那麼就會對 “title” 一列進行模糊查詢並返回對應的 BLOG 結果(細心的讀者可能會發現,“title” 的引數值需要包含查詢掩碼或萬用字元字元)。

如果希望通過 “title” 和 “author” 兩個引數進行可選搜尋該怎麼辦呢?首先,我想先將語句名稱修改成更名副其實的名稱;接下來,只需要加入另一個條件即可。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose、when、otherwise

有時候,我們不想使用所有的條件,而只是想從多個條件中選擇一個使用。針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。

還是上面的例子,但是策略變為:傳入了 “title” 就按 “title” 查詢,傳入了 “author” 就按 “author” 查詢的情形。若兩者都沒有傳入,就返回標記為 featured 的 BLOG(這可能是管理員認為,與其返回大量的無意義隨機 Blog,還不如返回一些由管理員精選的 Blog)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim、where、set

前面幾個例子已經方便地解決了一個臭名昭著的動態 SQL 問題。現在回到之前的 “if” 示例,這次我們將 “state = ‘ACTIVE’” 設定成動態條件,看看會發生什麼。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果沒有匹配的條件會怎麼樣?最終這條 SQL 會變成這樣:

SELECT * FROM BLOG
WHERE

這會導致查詢失敗。如果匹配的只是第二個條件又會怎樣?這條 SQL 會是這樣:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

這個查詢也會失敗。這個問題不能簡單地用條件元素來解決。這個問題是如此的難以解決,以至於解決過的人不會再想碰到這種問題。

MyBatis 有一個簡單且適合大多數場景的解決辦法。而在其他場景中,可以對其進行自定義以符合需求。而這,只需要一處簡單的改動:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只會在子元素返回任何內容的情況下才插入 “WHERE” 子句。而且,若子句的開頭為 “AND” 或 “OR”,where 元素也會將它們去除。

如果 where 元素與你期望的不太一樣,你也可以通過自定義 trim 元素來定製 where 元素的功能。比如,和 where 元素等價的自定義 trim 元素為:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 屬性會忽略通過管道符分隔的文字序列(注意此例中的空格是必要的)。上述例子會移除所有 prefixOverrides 屬性中指定的內容,並且插入 prefix 屬性中指定的內容。

用於動態更新語句的類似解決方案叫做 setset 元素可以用於動態包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

這個例子中,set 元素會動態地在行首插入 SET 關鍵字,並會刪掉額外的逗號(這些逗號是在使用條件語句給列賦值時引入的)。

來看看與 set 元素等價的自定義 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我們覆蓋了字尾值設定,並且自定義了字首值。

foreach

動態 SQL 的另一個常見使用場景是對集合進行遍歷(尤其是在構建 IN 條件語句的時候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 元素的功能非常強大,它允許你指定一個集合,宣告可以在元素體內使用的集合項(item)和索引(index)變數。它也允許你指定開頭與結尾的字串以及集合項迭代之間的分隔符。這個元素也不會錯誤地新增多餘的分隔符,看它多智慧!

提示 你可以將任何可迭代物件(如 List、Set 等)、Map 物件或者陣列物件作為集合引數傳遞給 foreach。當使用可迭代物件或者陣列時,index 是當前迭代的序號,item 的值是本次迭代獲取到的元素。當使用 Map 物件(或者 Map.Entry 物件的集合)時,index 是鍵,item 是值。

至此,我們已經完成了與 XML 配置及對映檔案相關的討論。下一章將詳細探討 Java API,以便你能充分利用已經建立的對映配置。

script

要在帶註解的對映器介面類中使用動態 SQL,可以使用 script 元素。比如:

    @Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
    void updateAuthorValues(Author author);

bind

bind 元素允許你在 OGNL 表示式以外建立一個變數,並將其繫結到當前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

多資料庫支援

如果配置了 databaseIdProvider,你就可以在動態程式碼中使用名為 “_databaseId” 的變數來為不同的資料庫構建特定的語句。比如下面的例子:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

動態 SQL 中的插入指令碼語言

MyBatis 從 3.2 版本開始支援插入指令碼語言,這允許你插入一種語言驅動,並基於這種語言來編寫動態 SQL 查詢語句。

可以通過實現以下介面來插入一種語言:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

實現自定義語言驅動後,你就可以在 mybatis-config.xml 檔案中將它設定為預設語言:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

或者,你也可以使用 lang 屬性為特定的語句指定語言:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

或者,在你的 mapper 介面上新增 @Lang 註解:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

Java API

既然你已經知道如何配置 MyBatis 以及如何建立對映,是時候來嚐點甜頭了。MyBatis 的 Java API 就是這個甜頭。稍後你將看到,和 JDBC 相比,MyBatis 大幅簡化你的程式碼併力圖保持其簡潔、容易理解和維護。為了使得 SQL 對映更加優秀,MyBatis 3 引入了許多重要的改進。

目錄結構

在我們深入 Java API 之前,理解關於目錄結構的最佳實踐是很重要的。MyBatis 非常靈活,你可以隨意安排你的檔案。但和其它框架一樣,目錄結構有一種最佳實踐。

讓我們看一下典型的應用目錄結構:

/my_application
  /bin
  /devlib
  /lib                <-- MyBatis *.jar 檔案在這裡。
  /src
    /org/myapp/
      /action
      /data           <-- MyBatis 配置檔案在這裡,包括對映器類、XML 配置、XML 對映檔案。
        /mybatis-config.xml
        /BlogMapper.java
        /BlogMapper.xml
      /model
      /service
      /view
    /properties       <-- 在 XML 配置中出現的屬性值在這裡。
  /test
    /org/myapp/
      /action
      /data
      /model
      /service
      /view
    /properties
  /web
    /WEB-INF
      /web.xml

當然,這是推薦的目錄結構,並非強制要求,但使用一個通用的目錄結構將更有利於大家溝通。

本章接下來的示例將假定你遵循這種目錄結構。

SqlSession

使用 MyBatis 的主要 Java 介面就是 SqlSession。你可以通過這個介面來執行命令,獲取對映器示例和管理事務。在介紹 SqlSession 介面之前,我們先來了解如何獲取一個 SqlSession 例項。SqlSessions 是由 SqlSessionFactory 例項建立的。SqlSessionFactory 物件包含建立 SqlSession 例項的各種方法。而 SqlSessionFactory 本身是由 SqlSessionFactoryBuilder 建立的,它可以從 XML、註解或 Java 配置程式碼來建立 SqlSessionFactory。

提示 當 Mybatis 與一些依賴注入框架(如 Spring 或者 Guice)搭配使用時,SqlSession 將被依賴注入框架建立並注入,所以你不需要使用 SqlSessionFactoryBuilder 或者 SqlSessionFactory,可以直接閱讀 SqlSession 這一節。請參考 Mybatis-Spring 或者 Mybatis-Guice 手冊以瞭解更多資訊。

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 有五個 build() 方法,每一種都允許你從不同的資源中建立一個 SqlSessionFactory 例項。

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

第一種方法是最常用的,它接受一個指向 XML 檔案(也就是之前討論的 mybatis-config.xml 檔案)的 InputStream 例項。可選的引數是 environment 和 properties。environment 決定載入哪種環境,包括資料來源和事務管理器。比如:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
        ...
    <dataSource type="POOLED">
        ...
  </environment>
  <environment id="production">
    <transactionManager type="MANAGED">
        ...
    <dataSource type="JNDI">
        ...
  </environment>
</environments>

如果你呼叫了帶 environment 引數的 build 方法,那麼 MyBatis 將使用該環境對應的配置。當然,如果你指定了一個無效的環境,會收到錯誤。如果你呼叫了不帶 environment 引數的 build 方法,那麼就會使用預設的環境配置(在上面的示例中,通過 default="development" 指定了預設環境)。

如果你呼叫了接受 properties 例項的方法,那麼 MyBatis 就會載入這些屬性,並在配置中提供使用。絕大多數場合下,可以用 ${propName} 形式引用這些配置值。

回想一下,在 mybatis-config.xml 中,可以引用屬性值,也可以直接指定屬性值。因此,理解屬性的優先順序是很重要的。在之前的文件中,我們已經介紹過了相關內容,但為了方便查閱,這裡再重新介紹一下:


如果一個屬性存在於下面的多個位置,那麼 MyBatis 將按照以下順序來載入它們:

  • 首先,讀取在 properties 元素體中指定的屬性;
  • 其次,讀取在 properties 元素的類路徑 resource 或 url 指定的屬性,且會覆蓋已經指定了的重複屬性;
  • 最後,讀取作為方法引數傳遞的屬性,且會覆蓋已經從 properties 元素體和 resource 或 url 屬性中載入了的重複屬性。

因此,通過方法引數傳遞的屬性的優先順序最高,resource 或 url 指定的屬性優先順序中等,在 properties 元素體中指定的屬性優先順序最低。


總結一下,前四個方法很大程度上是相同的,但提供了不同的覆蓋選項,允許你可選地指定 environment 和/或 properties。以下給出一個從 mybatis-config.xml 檔案建立 SqlSessionFactory 的示例:

String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

注意,這裡我們使用了 Resources 工具類,這個類在 org.apache.ibatis.io 包中。Resources 類正如其名,會幫助你從類路徑下、檔案系統或一個 web URL 中載入資原始檔。在略讀該類的原始碼或用 IDE 檢視該類資訊後,你會發現一整套相當實用的方法。這裡給出一個簡表:

URL getResourceURL(String resource)
URL getResourceURL(ClassLoader loader, String resource)
InputStream getResourceAsStream(String resource)
InputStream getResourceAsStream(ClassLoader loader, String resource)
Properties getResourceAsProperties(String resource)
Properties getResourceAsProperties(ClassLoader loader, String resource)
Reader getResourceAsReader(String resource)
Reader getResourceAsReader(ClassLoader loader, String resource)
File getResourceAsFile(String resource)
File getResourceAsFile(ClassLoader loader, String resource)
InputStream getUrlAsStream(String urlString)
Reader getUrlAsReader(String urlString)
Properties getUrlAsProperties(String urlString)
Class classForName(String className)

最後一個 build 方法接受一個 Configuration 例項。Configuration 類包含了對一個 SqlSessionFactory 例項你可能關心的所有內容。在檢查配置時,Configuration 類很有用,它允許你查詢和操縱 SQL 對映(但當應用開始接收請求時不推薦使用)。你之前學習過的所有配置開關都存在於 Configuration 類,只不過它們是以 Java API 形式暴露的。以下是一個簡單的示例,演示如何手動配置 Configuration 例項,然後將它傳遞給 build() 方法來建立 SqlSessionFactory。

DataSource dataSource = BaseDataTest.createBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();

Environment environment = new Environment("development", transactionFactory, dataSource);

Configuration configuration = new Configuration(environment);
configuration.setLazyLoadingEnabled(true);
configuration.setEnhancementEnabled(true);
configuration.getTypeAliasRegistry().registerAlias(Blog.class);
configuration.getTypeAliasRegistry().registerAlias(Post.class);
configuration.getTypeAliasRegistry().registerAlias(Author.class);
configuration.addMapper(BoundBlogMapper.class);
configuration.addMapper(BoundAuthorMapper.class);

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(configuration);

現在你就獲得一個可以用來建立 SqlSession 例項的 SqlSessionFactory 了。

SqlSessionFactory

SqlSessionFactory 有六個方法建立 SqlSession 例項。通常來說,當你選擇其中一個方法時,你需要考慮以下幾點:

  • 事務處理:你希望在 session 作用域中使用事務作用域,還是使用自動提交(auto-commit)?(對很多資料庫和/或 JDBC 驅動來說,等同於關閉事務支援)
  • 資料庫連線:你希望 MyBatis 幫你從已配置的資料來源獲取連線,還是使用自己提供的連線?
  • 語句執行:你希望 MyBatis 複用 PreparedStatement 和/或批量更新語句(包括插入語句和刪除語句)嗎?

基於以上需求,有下列已過載的多個 openSession() 方法供使用。

SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

預設的 openSession() 方法沒有引數,它會建立具備如下特性的 SqlSession:

  • 事務作用域將會開啟(也就是不自動提交)。
  • 將由當前環境配置的 DataSource 例項中獲取 Connection 物件。
  • 事務隔離級別將會使用驅動或資料來源的預設設定。
  • 預處理語句不會被複用,也不會批量處理更新。

相信你已經能從方法簽名中知道這些方法的區別。向 autoCommit 可選引數傳遞 true 值即可開啟自動提交功能。若要使用自己的 Connection 例項,傳遞一個 Connection 例項給 connection 引數即可。注意,我們沒有提供同時設定 ConnectionautoCommit 的方法,這是因為 MyBatis 會依據傳入的 Connection 來決定是否啟用 autoCommit。對於事務隔離級別,MyBatis 使用了一個 Java 列舉包裝器來表示,稱為 TransactionIsolationLevel,事務隔離級別支援 JDBC 的五個隔離級別(NONEREAD_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE),並且與預期的行為一致。

你可能對 ExecutorType 引數感到陌生。這個列舉型別定義了三個值:

  • ExecutorType.SIMPLE:該型別的執行器沒有特別的行為。它為每個語句的執行建立一個新的預處理語句。
  • ExecutorType.REUSE:該型別的執行器會複用預處理語句。
  • ExecutorType.BATCH:該型別的執行器會批量執行所有更新語句,如果 SELECT 在多個更新中間執行,將在必要時將多條更新語句分隔開來,以方便理解。

提示 在 SqlSessionFactory 中還有一個方法我們沒有提及,就是 getConfiguration()。這個方法會返回一個 Configuration 例項,你可以在執行時使用它來檢查 MyBatis 的配置。

提示 如果你使用過 MyBatis 的舊版本,可能還記得 session、事務和批量操作是相互獨立的。在新版本中則不是這樣。上述三者都包含在 session 作用域內。你不必分別處理事務或批量操作就能得到想要的全部效果。

SqlSession

正如之前所提到的,SqlSession 在 MyBatis 中是非常強大的一個類。它包含了所有執行語句、提交或回滾事務以及獲取對映器例項的方法。

SqlSession 類的方法超過了 20 個,為了方便理解,我們將它們分成幾種組別。

語句執行方法

這些方法被用來執行定義在 SQL 對映 XML 檔案中的 SELECT、INSERT、UPDATE 和 DELETE 語句。你可以通過名字快速瞭解它們的作用,每一方法都接受語句的 ID 以及引數物件,引數可以是原始型別(支援自動裝箱或包裝類)、JavaBean、POJO 或 Map。

<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
<T> Cursor<T> selectCursor(String statement, Object parameter)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

selectOne 和 selectList 的不同僅僅是 selectOne 必須返回一個物件或 null 值。如果返回值多於一個,就會丟擲異常。如果你不知道返回物件會有多少,請使用 selectList。如果需要檢視某個物件是否存在,最好的辦法是查詢一個 count 值(0 或 1)。selectMap 稍微特殊一點,它會將返回物件的其中一個屬性作為 key 值,將物件作為 value 值,從而將多個結果集轉為 Map 型別值。由於並不是所有語句都需要引數,所以這些方法都具有一個不需要引數的過載形式。

遊標(Cursor)與列表(List)返回的結果相同,不同的是,遊標藉助迭代器實現了資料的惰性載入。

try (Cursor<MyEntity> entities = session.selectCursor(statement, param)) {
   for (MyEntity entity:entities) {
      // 處理單個實體
   }
}

insert、update 以及 delete 方法返回的值表示受該語句影響的行數。

<T> T selectOne(String statement)
<E> List<E> selectList(String statement)
<T> Cursor<T> selectCursor(String statement)
<K,V> Map<K,V> selectMap(String statement, String mapKey)
int insert(String statement)
int update(String statement)
int delete(String statement)

最後,還有 select 方法的三個高階版本,它們允許你限制返回行數的範圍,或是提供自定義結果處理邏輯,通常在資料集非常龐大的情形下使用。

<E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
void select (String statement, Object parameter, ResultHandler<T> handler)
void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)

RowBounds 引數會告訴 MyBatis 略過指定數量的記錄,並限制返回結果的數量。RowBounds 類的 offset 和 limit 值只有在建構函式時才能傳入,其它時候是不能修改的。

int offset = 100;
int limit = 25;
RowBounds rowBounds = new RowBounds(offset, limit);

資料庫驅動決定了略過記錄時的查詢效率。為了獲得最佳的效能,建議將 ResultSet 型別設定為 SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE(換句話說:不要使用 FORWARD_ONLY)。

ResultHandler 引數允許自定義每行結果的處理過程。你可以將它新增到 List 中、建立 Map 和 Set,甚至丟棄每個返回值,只保留計算後的統計結果。你可以使用 ResultHandler 做很多事,這其實就是 MyBatis 構建 結果列表的內部實現辦法。

從版本 3.4.6 開始,ResultHandler 會在儲存過程的 REFCURSOR 輸出引數中傳遞使用的 CALLABLE 語句。

它的介面很簡單:

package org.apache.ibatis.session;
public interface ResultHandler<T> {
  void handleResult(ResultContext<? extends T> context);
}

ResultContext 引數允許你訪問結果物件和當前已被建立的物件數目,另外還提供了一個返回值為 Boolean 的 stop 方法,你可以使用此 stop 方法來停止 MyBatis 載入更多的結果。

使用 ResultHandler 的時候需要注意以下兩個限制:

  • 使用帶 ResultHandler 引數的方法時,收到的資料不會被快取。
  • 當使用高階的結果對映集(resultMap)時,MyBatis 很可能需要數行結果來構造一個物件。如果你使用了 ResultHandler,你可能會接收到關聯(association)或者集合(collection)中尚未被完整填充的物件。
立即批量更新方法

當你將 ExecutorType 設定為 ExecutorType.BATCH 時,可以使用這個方法清除(執行)快取在 JDBC 驅動類中的批量更新語句。

List<BatchResult> flushStatements()
事務控制方法

有四個方法用來控制事務作用域。當然,如果你已經設定了自動提交或你使用了外部事務管理器,這些方法就沒什麼作用了。然而,如果你正在使用由 Connection 例項控制的 JDBC 事務管理器,那麼這四個方法就會派上用場:

void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)

預設情況下 MyBatis 不會自動提交事務,除非它偵測到呼叫了插入、更新或刪除方法改變了資料庫。如果你沒有使用這些方法提交修改,那麼你可以在 commit 和 rollback 方法引數中傳入 true 值,來保證事務被正常提交(注意,在自動提交模式或者使用了外部事務管理器的情況下,設定 force 值對 session 無效)。大部分情況下你無需呼叫 rollback(),因為 MyBatis 會在你沒有呼叫 commit 時替你完成回滾操作。不過,當你要在一個可能多次提交或回滾的 session 中詳細控制事務,回滾操作就派上用場了。

提示 MyBatis-Spring 和 MyBatis-Guice 提供了宣告式事務處理,所以如果你在使用 Mybatis 的同時使用了 Spring 或者 Guice,請參考它們的手冊以獲取更多的內容。

本地快取

Mybatis 使用到了兩種快取:本地快取(local cache)和二級快取(second level cache)。

每當一個新 session 被建立,MyBatis 就會建立一個與之相關聯的本地快取。任何在 session 執行過的查詢結果都會被儲存在本地快取中,所以,當再次執行引數相同的相同查詢時,就不需要實際查詢資料庫了。本地快取將會在做出修改、事務提交或回滾,以及關閉 session 時清空。

預設情況下,本地快取資料的生命週期等同於整個 session 的週期。由於快取會被用來解決迴圈引用問題和加快重複巢狀查詢的速度,所以無法將其完全禁用。但是你可以通過設定 localCacheScope=STATEMENT 來只在語句執行時使用快取。

注意,如果 localCacheScope 被設定為 SESSION,對於某個物件,MyBatis 將返回在本地快取中唯一物件的引用。對返回的物件(例如 list)做出的任何修改將會影響本地快取的內容,進而將會影響到在本次 session 中從快取返回的值。因此,不要對 MyBatis 所返回的物件作出更改,以防後患。

你可以隨時呼叫以下方法來清空本地快取:

void clearCache()
確保 SqlSession 被關閉
void close()

對於你開啟的任何 session,你都要保證它們被妥善關閉,這很重要。保證妥善關閉的最佳程式碼模式是這樣的:

SqlSession session = sqlSessionFactory.openSession();
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 假設下面三行程式碼是你的業務邏輯
    session.insert(...);
    session.update(...);
    session.delete(...);
    session.commit();
}

提示 和 SqlSessionFactory 一樣,你可以呼叫當前使用的 SqlSession 的 getConfiguration 方法來獲得 Configuration 例項。

Configuration getConfiguration()
使用對映器
<T> T getMapper(Class<T> type)

上述的各個 insert、update、delete 和 select 方法都很強大,但也有些繁瑣,它們並不符合型別安全,對你的 IDE 和單元測試也不是那麼友好。因此,使用對映器類來執行對映語句是更常見的做法。

我們已經在之前的入門章節中見到過一個使用對映器的示例。一個對映器類就是一個僅需宣告與 SqlSession 方法相匹配方法的介面。下面的示例展示了一些方法簽名以及它們是如何對映到 SqlSession 上的。

public interface AuthorMapper {
  // (Author) selectOne("selectAuthor",5);
  Author selectAuthor(int id);
  // (List<Author>) selectList(“selectAuthors”)
  List<Author> selectAuthors();
  // (Map<Integer,Author>) selectMap("selectAuthors", "id")
  @MapKey("id")
  Map<Integer, Author> selectAuthors();
  // insert("insertAuthor", author)
  int insertAuthor(Author author);
  // updateAuthor("updateAuthor", author)
  int updateAuthor(Author author);
  // delete("deleteAuthor",5)
  int deleteAuthor(int id);
}

總之,每個對映器方法簽名應該匹配相關聯的 SqlSession 方法,字串引數 ID 無需匹配。而是由方法名匹配對映語句的 ID。

此外,返回型別必須匹配期望的結果型別,返回單個值時,返回型別應該是返回值的類,返回多個值時,則為陣列或集合類,另外也可以是遊標(Cursor)。所有常用的型別都是支援的,包括:原始型別、Map、POJO 和 JavaBean。

提示 對映器介面不需要去實現任何介面或繼承自任何類。只要方法簽名可以被用來唯一識別對應的對映語句就可以了。

提示 對映器介面可以繼承自其他介面。在使用 XML 來繫結對映器介面時,保證語句處於合適的名稱空間中即可。唯一的限制是,不能在兩個具有繼承關係的介面中擁有相同的方法簽名(這是潛在的危險做法,不可取)。

你可以傳遞多個引數給一個對映器方法。在多個引數的情況下,預設它們將會以 param 加上它們在引數列表中的位置來命名,比如:#{param1}、#{param2}等。如果你想(在有多個引數時)自定義引數的名稱,那麼你可以在引數上使用 @Param("paramName") 註解。

你也可以給方法傳遞一個 RowBounds 例項來限制查詢結果。

對映器註解

設計初期的 MyBatis 是一個 XML 驅動的框架。配置資訊是基於 XML 的,對映語句也是定義在 XML 中的。而在 MyBatis 3 中,我們提供了其它的配置方式。MyBatis 3 構建在全面且強大的基於 Java 語言的配置 API 之上。它是 XML 和註解配置的基礎。註解提供了一種簡單且低成本的方式來實現簡單的對映語句。

提示 不幸的是,Java 註解的表達能力和靈活性十分有限。儘管我們花了很多時間在調查、設計和試驗上,但最強大的 MyBatis 對映並不能用註解來構建——我們真沒開玩笑。而 C# 屬性就沒有這些限制,因此 MyBatis.NET 的配置會比 XML 有更大的選擇餘地。雖說如此,基於 Java 註解的配置還是有它的好處的。

註解如下表所示:

註解 使用物件 XML 等價形式 描述
@CacheNamespace <cache> 為給定的名稱空間(比如類)配置快取。屬性:implemetationevictionflushIntervalsizereadWriteblockingproperties
@Property N/A <property> 指定引數值或佔位符(placeholder)(該佔位符能被 mybatis-config.xml 內的配置屬性替換)。屬性:namevalue。(僅在 MyBatis 3.4.2 以上可用)
@CacheNamespaceRef <cacheRef> 引用另外一個名稱空間的快取以供使用。注意,即使共享相同的全限定類名,在 XML 對映檔案中宣告的快取仍被識別為一個獨立的名稱空間。屬性:valuename。如果你使用了這個註解,你應設定 value 或者 name 屬性的其中一個。value 屬性用於指定能夠表示該名稱空間的 Java 型別(名稱空間名就是該 Java 型別的全限定類名),name 屬性(這個屬性僅在 MyBatis 3.4.2 以上可用)則直接指定了名稱空間的名字。
@ConstructorArgs 方法 <constructor> 收集一組結果以傳遞給一個結果物件的構造方法。屬性:value,它是一個 Arg 陣列。
@Arg N/A <arg>``<idArg> ConstructorArgs 集合的一部分,代表一個構造方法引數。屬性:idcolumnjavaTypejdbcTypetypeHandlerselectresultMap。id 屬性和 XML 元素 <idArg> 相似,它是一個布林值,表示該屬性是否用於唯一標識和比較物件。從版本 3.5.4 開始,該註解變為可重複註解。
@TypeDiscriminator 方法 <discriminator> 決定使用何種結果對映的一組取值(case)。屬性:columnjavaTypejdbcTypetypeHandlercases。cases 屬性是一個 Case 的陣列。
@Case N/A <case> 表示某個值的一個取值以及該取值對應的對映。屬性:valuetyperesults。results 屬性是一個 Results 的陣列,因此這個註解實際上和 ResultMap 很相似,由下面的 Results 註解指定。
@Results 方法 <resultMap> 一組結果對映,指定了對某個特定結果列,對映到某個屬性或欄位的方式。屬性:valueid。value 屬性是一個 Result 註解的陣列。而 id 屬性則是結果對映的名稱。從版本 3.5.4 開始,該註解變為可重複註解。
@Result N/A <result>``<id> 在列和屬性或欄位之間的單個結果對映。屬性:idcolumnjavaTypejdbcTypetypeHandleronemany。id 屬性和 XML 元素 <id> 相似,它是一個布林值,表示該屬性是否用於唯一標識和比較物件。one 屬性是一個關聯,和 <association> 類似,而 many 屬性則是集合關聯,和 <collection> 類似。這樣命名是為了避免產生名稱衝突。
@One N/A <association> 複雜型別的單個屬性對映。屬性: select,指定可載入合適型別例項的對映語句(也就是對映器方法)全限定名; fetchType,指定在該對映中覆蓋全域性配置引數 lazyLoadingEnabledresultMap(available since 3.5.5), which is the fully qualified name of a result map that map to a single container object from select result; columnPrefix(available since 3.5.5), which is column prefix for grouping select columns at nested result map. 提示 註解 API 不支援聯合對映。這是由於 Java 註解不允許產生迴圈引用。
@Many N/A <collection> 複雜型別的集合屬性對映。屬性: select,指定可載入合適型別例項集合的對映語句(也就是對映器方法)全限定名; fetchType,指定在該對映中覆蓋全域性配置引數 lazyLoadingEnabled resultMap(available since 3.5.5), which is the fully qualified name of a result map that map to collection object from select result; columnPrefix(available since 3.5.5), which is column prefix for grouping select columns at nested result map. 提示 註解 API 不支援聯合對映。這是由於 Java 註解不允許產生迴圈引用。
@MapKey 方法 供返回值為 Map 的方法使用的註解。它使用物件的某個屬性作為 key,將物件 List 轉化為 Map。屬性:value,指定作為 Map 的 key 值的物件屬性名。
@Options 方法 對映語句的屬性 該註解允許你指定大部分開關和配置選項,它們通常在對映語句上作為屬性出現。與在註解上提供大量的屬性相比,Options 註解提供了一致、清晰的方式來指定選項。屬性:useCache=trueflushCache=FlushCachePolicy.DEFAULTresultSetType=DEFAULTstatementType=PREPAREDfetchSize=-1timeout=-1useGeneratedKeys=falsekeyProperty=""keyColumn=""resultSets="", databaseId=""。注意,Java 註解無法指定 null 值。因此,一旦你使用了 Options 註解,你的語句就會被上述屬性的預設值所影響。要注意避免預設值帶來的非預期行為。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis use the Options with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded. 注意:keyColumn 屬性只在某些資料庫中有效(如 Oracle、PostgreSQL 等)。要了解更多關於 keyColumnkeyProperty 可選值資訊,請檢視“insert, update 和 delete”一節。
@Insert``@Update``@Delete``@Select 方法 <insert>``<update>``<delete>``<select> 每個註解分別代表將會被執行的 SQL 語句。它們用字串陣列(或單個字串)作為引數。如果傳遞的是字串陣列,字串陣列會被連線成單個完整的字串,每個字串之間加入一個空格。這有效地避免了用 Java 程式碼構建 SQL 語句時產生的“丟失空格”問題。當然,你也可以提前手動連線好字串。屬性:value,指定用來組成單個 SQL 語句的字串陣列。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis use a statement with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@InsertProvider``@UpdateProvider``@DeleteProvider``@SelectProvider 方法 <insert>``<update>``<delete>``<select> 允許構建動態 SQL。這些備選的 SQL 註解允許你指定返回 SQL 語句的類和方法,以供執行時執行。(從 MyBatis 3.4.6 開始,可以使用 CharSequence 代替 String 來作為返回型別)。當執行對映語句時,MyBatis 會例項化註解指定的類,並呼叫註解指定的方法。你可以通過 ProviderContext 傳遞對映方法接收到的引數、"Mapper interface type" 和 "Mapper method"(僅在 MyBatis 3.4.5 以上支援)作為引數。(MyBatis 3.4 以上支援傳入多個引數) 屬性:valuetypemethoddatabaseIdvalue and type 屬性用於指定類名 (The type attribute is alias for value, you must be specify either one. But both attributes can be omit when specify the defaultSqlProviderType as global configuration)。 method 用於指定該類的方法名(從版本 3.5.1 開始,可以省略 method 屬性,MyBatis 將會使用 ProviderMethodResolver 介面解析方法的具體實現。如果解析失敗,MyBatis 將會使用名為 provideSql 的降級實現)。提示 接下來的“SQL 語句構建器”一章將會討論該話題,以幫助你以更清晰、更便於閱讀的方式構建動態 SQL。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis will use a provider method with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@Param 引數 N/A 如果你的對映方法接受多個引數,就可以使用這個註解自定義每個引數的名字。否則在預設情況下,除 RowBounds 以外的引數會以 "param" 加引數位置被命名。例如 #{param1}, #{param2}。如果使用了 @Param("person"),引數就會被命名為 #{person}
@SelectKey 方法 <selectKey> 這個註解的功能與 <selectKey> 標籤完全一致。該註解只能在 @Insert@InsertProvider@Update@UpdateProvider 標註的方法上使用,否則將會被忽略。如果標註了 @SelectKey 註解,MyBatis 將會忽略掉由 @Options 註解所設定的生成主鍵或設定(configuration)屬性。屬性:statement 以字串陣列形式指定將會被執行的 SQL 語句,keyProperty 指定作為引數傳入的物件對應屬性的名稱,該屬性將會更新成新的值,before 可以指定為 truefalse 以指明 SQL 語句應被在插入語句的之前還是之後執行。resultType 則指定 keyProperty 的 Java 型別。statementType 則用於選擇語句型別,可以選擇 STATEMENTPREPAREDCALLABLE 之一,它們分別對應於 StatementPreparedStatementCallableStatement。預設值是 PREPARED。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis will use a statement with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@ResultMap 方法 N/A 這個註解為 @Select 或者 @SelectProvider 註解指定 XML 對映中 <resultMap> 元素的 id。這使得註解的 select 可以複用已在 XML 中定義的 ResultMap。如果標註的 select 註解中存在 @Results 或者 @ConstructorArgs 註解,這兩個註解將被此註解覆蓋。
@ResultType 方法 N/A 在使用了結果處理器的情況下,需要使用此註解。由於此時的返回型別為 void,所以 Mybatis 需要有一種方法來判斷每一行返回的物件型別。如果在 XML 有對應的結果對映,請使用 @ResultMap 註解。如果結果型別在 XML 的 <select> 元素中指定了,就不需要使用其它註解了。否則就需要使用此註解。比如,如果一個標註了 @Select 的方法想要使用結果處理器,那麼它的返回型別必須是 void,並且必須使用這個註解(或者 @ResultMap)。這個註解僅在方法返回型別是 void 的情況下生效。
@Flush 方法 N/A 如果使用了這個註解,定義在 Mapper 介面中的方法就能夠呼叫 SqlSession#flushStatements() 方法。(Mybatis 3.3 以上可用)
對映註解示例

這個例子展示瞭如何使用 @SelectKey 註解來在插入前讀取資料庫序列的值:

@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);

這個例子展示瞭如何使用 @SelectKey 註解來在插入後讀取資料庫自增列的值:

@Insert("insert into table2 (name) values(#{name})")
@SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class)
int insertTable2(Name name);

這個例子展示瞭如何使用 @Flush 註解來呼叫 SqlSession#flushStatements()

@Flush
List<BatchResult> flush();

這些例子展示瞭如何通過指定 @Result 的 id 屬性來命名結果集:

@Results(id = "userResult", value = {
  @Result(property = "id", column = "uid", id = true),
  @Result(property = "firstName", column = "first_name"),
  @Result(property = "lastName", column = "last_name")
})
@Select("select * from users where id = #{id}")
User getUserById(Integer id);

@Results(id = "companyResults")
@ConstructorArgs({
  @Arg(column = "cid", javaType = Integer.class, id = true),
  @Arg(column = "name", javaType = String.class)
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);

這個例子展示瞭如何使用單個引數的 @SqlProvider 註解:

@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);

class UserSqlBuilder {
  public static String buildGetUsersByName(final String name) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      if (name != null) {
        WHERE("name like #{value} || '%'");
      }
      ORDER_BY("id");
    }}.toString();
  }
}

這個例子展示瞭如何使用多個引數的 @SqlProvider 註解:

@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(
    @Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

  // 如果不使用 @Param,就應該定義與 mapper 方法相同的引數
  public static String buildGetUsersByName(
      final String name, final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }

  // 如果使用 @Param,就可以只定義需要使用的引數
  public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }
}

This example shows usage that share an sql provider class to all mapper methods using global configuration(Available since 3.5.6):

Configuration configuration = new Configuration();
configuration.setDefaultSqlProviderType(TemplateFilePathProvider.class); // Specify an sql provider class for sharing on all mapper methods
// ...
// Can omit the type/value attribute on sql provider annotation
// If omit it, the MyBatis apply the class that specified on defaultSqlProviderType.
public interface UserMapper {

  @SelectProvider // Same with @SelectProvider(TemplateFilePathProvider.class)
  User findUser(int id);

  @InsertProvider // Same with @InsertProvider(TemplateFilePathProvider.class)
  void createUser(User user);

  @UpdateProvider // Same with @UpdateProvider(TemplateFilePathProvider.class)
  void updateUser(User user);

  @DeleteProvider // Same with @DeleteProvider(TemplateFilePathProvider.class)
  void deleteUser(int id);
}

以下例子展示了 ProviderMethodResolver(3.5.1 後可用)的預設實現使用方法:

@SelectProvider(UserSqlProvider.class)
List<User> getUsersByName(String name);

// 在你的 provider 類中實現 ProviderMethodResolver 介面
class UserSqlProvider implements ProviderMethodResolver {
  // 預設實現中,會將對映器方法的呼叫解析到實現的同名方法上
  public static String getUsersByName(final String name) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      if (name != null) {
        WHERE("name like #{value} || '%'");
      }
      ORDER_BY("id");
    }}.toString();
  }
}

This example shows usage the databaseId attribute on the statement annotation(Available since 3.5.5):

@Select(value = "SELECT SYS_GUID() FROM dual", databaseId = "oracle") // Use this statement if DatabaseIdProvider provide "oracle"
@Select(value = "SELECT uuid_generate_v4()", databaseId = "postgres") // Use this statement if DatabaseIdProvider provide "postgres"
@Select("SELECT RANDOM_UUID()") // Use this statement if the DatabaseIdProvider not configured or not matches databaseId
String generateId();

SQL 語句構建器

問題

Java 程式設計師面對的最痛苦的事情之一就是在 Java 程式碼中嵌入 SQL 語句。這通常是因為需要動態生成 SQL 語句,不然我們可以將它們放到外部檔案或者儲存過程中。如你所見,MyBatis 在 XML 對映中具備強大的 SQL 動態生成能力。但有時,我們還是需要在 Java 程式碼裡構建 SQL 語句。此時,MyBatis 有另外一個特性可以幫到你,讓你從處理典型問題中解放出來,比如加號、引號、換行、格式化問題、嵌入條件的逗號管理及 AND 連線。確實,在 Java 程式碼中動態生成 SQL 程式碼真的就是一場噩夢。例如:

String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
"P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
"FROM PERSON P, ACCOUNT A " +
"INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID " +
"INNER JOIN COMPANY C on D.COMPANY_ID = C.ID " +
"WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) " +
"OR (P.LAST_NAME like ?) " +
"GROUP BY P.ID " +
"HAVING (P.LAST_NAME like ?) " +
"OR (P.FIRST_NAME like ?) " +
"ORDER BY P.ID, P.FULL_NAME";

解決方案

MyBatis 3 提供了方便的工具類來幫助解決此問題。藉助 SQL 類,我們只需要簡單地建立一個例項,並呼叫它的方法即可生成 SQL 語句。讓我們來用 SQL 類重寫上面的例子:

private String selectPersonSql() {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
    FROM("PERSON P");
    FROM("ACCOUNT A");
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
    WHERE("P.ID = A.ID");
    WHERE("P.FIRST_NAME like ?");
    OR();
    WHERE("P.LAST_NAME like ?");
    GROUP_BY("P.ID");
    HAVING("P.LAST_NAME like ?");
    OR();
    HAVING("P.FIRST_NAME like ?");
    ORDER_BY("P.ID");
    ORDER_BY("P.FULL_NAME");
  }}.toString();
}

這個例子有什麼特別之處嗎?仔細看一下你會發現,你不用擔心可能會重複出現的 "AND" 關鍵字,或者要做出用 "WHERE" 拼接還是 "AND" 拼接還是不用拼接的選擇。SQL 類已經為你處理了哪裡應該插入 "WHERE"、哪裡應該使用 "AND" 的問題,並幫你完成所有的字串拼接工作。

SQL 類

這裡有一些示例:

// 匿名內部類風格
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent 風格
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// 動態條件(注意引數需要使用 final 修飾,以便匿名內部類對它們進行訪問)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
    FROM("PERSON P");
    if (id != null) {
      WHERE("P.ID like #{id}");
    }
    if (firstName != null) {
      WHERE("P.FIRST_NAME like #{firstName}");
    }
    if (lastName != null) {
      WHERE("P.LAST_NAME like #{lastName}");
    }
    ORDER_BY("P.LAST_NAME");
  }}.toString();
}

public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

public String insertPersonSql() {
  return new SQL() {{
    INSERT_INTO("PERSON");
    VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
    VALUES("LAST_NAME", "#{lastName}");
  }}.toString();
}

public String updatePersonSql() {
  return new SQL() {{
    UPDATE("PERSON");
    SET("FIRST_NAME = #{firstName}");
    WHERE("ID = #{id}");
  }}.toString();
}
方法 描述
SELECT(String)``SELECT(String...) 開始新的或追加到已有的 SELECT子句。可以被多次呼叫,引數會被追加到 SELECT 子句。 引數通常使用逗號分隔的列名和別名列表,但也可以是資料庫驅動程式接受的任意引數。
SELECT_DISTINCT(String)``SELECT_DISTINCT(String...) 開始新的或追加到已有的 SELECT子句,並新增 DISTINCT 關鍵字到生成的查詢中。可以被多次呼叫,引數會被追加到 SELECT 子句。 引數通常使用逗號分隔的列名和別名列表,但也可以是資料庫驅動程式接受的任意引數。
FROM(String)``FROM(String...) 開始新的或追加到已有的 FROM子句。可以被多次呼叫,引數會被追加到 FROM子句。 引數通常是一個表名或別名,也可以是資料庫驅動程式接受的任意引數。
JOIN(String)``JOIN(String...)``INNER_JOIN(String)``INNER_JOIN(String...)``LEFT_OUTER_JOIN(String)``LEFT_OUTER_JOIN(String...)``RIGHT_OUTER_JOIN(String)``RIGHT_OUTER_JOIN(String...) 基於呼叫的方法,新增新的合適型別的 JOIN 子句。 引數可以包含一個由列和連線條件構成的標準連線。
WHERE(String)``WHERE(String...) 插入新的 WHERE 子句條件,並使用 AND 拼接。可以被多次呼叫,對於每一次呼叫產生的新條件,會使用 AND 拼接起來。要使用 OR 分隔,請使用 OR()
OR() 使用 OR 來分隔當前的 WHERE 子句條件。 可以被多次呼叫,但在一行中多次呼叫會生成錯誤的 SQL
AND() 使用 AND 來分隔當前的 WHERE子句條件。 可以被多次呼叫,但在一行中多次呼叫會生成錯誤的 SQL。由於 WHEREHAVING都會自動使用 AND 拼接, 因此這個方法並不常用,只是為了完整性才被定義出來。
GROUP_BY(String)``GROUP_BY(String...) 追加新的 GROUP BY 子句,使用逗號拼接。可以被多次呼叫,每次呼叫都會使用逗號將新的條件拼接起來。
HAVING(String)``HAVING(String...) 追加新的 HAVING 子句。使用 AND 拼接。可以被多次呼叫,每次呼叫都使用AND來拼接新的條件。要使用 OR 分隔,請使用 OR()
ORDER_BY(String)``ORDER_BY(String...) 追加新的 ORDER BY 子句,使用逗號拼接。可以多次被呼叫,每次呼叫會使用逗號拼接新的條件。
LIMIT(String)``LIMIT(int) 追加新的 LIMIT 子句。 僅在 SELECT()、UPDATE()、DELETE() 時有效。 當在 SELECT() 中使用時,應該配合 OFFSET() 使用。(於 3.5.2 引入)
OFFSET(String)``OFFSET(long) 追加新的 OFFSET 子句。 僅在 SELECT() 時有效。 當在 SELECT() 時使用時,應該配合 LIMIT() 使用。(於 3.5.2 引入)
OFFSET_ROWS(String)``OFFSET_ROWS(long) 追加新的 OFFSET n ROWS 子句。 僅在 SELECT() 時有效。 該方法應該配合 FETCH_FIRST_ROWS_ONLY() 使用。(於 3.5.2 加入)
FETCH_FIRST_ROWS_ONLY(String)``FETCH_FIRST_ROWS_ONLY(int) 追加新的 FETCH FIRST n ROWS ONLY 子句。 僅在 SELECT() 時有效。 該方法應該配合 OFFSET_ROWS() 使用。(於 3.5.2 加入)
DELETE_FROM(String) 開始新的 delete 語句,並指定刪除表的表名。通常它後面都會跟著一個 WHERE 子句!
INSERT_INTO(String) 開始新的 insert 語句,並指定插入資料表的表名。後面應該會跟著一個或多個 VALUES() 呼叫,或 INTO_COLUMNS() 和 INTO_VALUES() 呼叫。
SET(String)``SET(String...) 對 update 語句追加 "set" 屬性的列表
UPDATE(String) 開始新的 update 語句,並指定更新表的表名。後面都會跟著一個或多個 SET() 呼叫,通常也會有一個 WHERE() 呼叫。
VALUES(String, String) 追加資料值到 insert 語句中。第一個引數是資料插入的列名,第二個引數則是資料值。
INTO_COLUMNS(String...) 追加插入列子句到 insert 語句中。應與 INTO_VALUES() 一同使用。
INTO_VALUES(String...) 追加插入值子句到 insert 語句中。應與 INTO_COLUMNS() 一同使用。
ADD_ROW() 新增新的一行資料,以便執行批量插入。(於 3.5.2 引入)

提示 注意,SQL 類將原樣插入 LIMITOFFSETOFFSET n ROWS 以及 FETCH FIRST n ROWS ONLY 子句。換句話說,類庫不會為不支援這些子句的資料庫執行任何轉換。 因此,使用者應該要了解目標資料庫是否支援這些子句。如果目標資料庫不支援這些子句,產生的 SQL 可能會引起執行錯誤。

從版本 3.4.2 開始,你可以像下面這樣使用可變長度引數:

public String selectPersonSql() {
  return new SQL()
    .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
    .FROM("PERSON P", "ACCOUNT A")
    .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
    .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
    .ORDER_BY("P.ID", "P.FULL_NAME")
    .toString();
}

public String insertPersonSql() {
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{id}", "#{fullName}")
    .toString();
}

public String updatePersonSql() {
  return new SQL()
    .UPDATE("PERSON")
    .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
    .WHERE("ID = #{id}")
    .toString();
}

從版本 3.5.2 開始,你可以像下面這樣構建批量插入語句:

public String insertPersonsSql() {
  // INSERT INTO PERSON (ID, FULL_NAME)
  //     VALUES (#{mainPerson.id}, #{mainPerson.fullName}) , (#{subPerson.id}, #{subPerson.fullName})
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{mainPerson.id}", "#{mainPerson.fullName}")
    .ADD_ROW()
    .INTO_VALUES("#{subPerson.id}", "#{subPerson.fullName}")
    .toString();
}

從版本 3.5.2 開始,你可以像下面這樣構建限制返回結果數的 SELECT 語句,:

public String selectPersonsWithOffsetLimitSql() {
  // SELECT id, name FROM PERSON
  //     LIMIT #{limit} OFFSET #{offset}
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .LIMIT("#{limit}")
    .OFFSET("#{offset}")
    .toString();
}

public String selectPersonsWithFetchFirstSql() {
  // SELECT id, name FROM PERSON
  //     OFFSET #{offset} ROWS FETCH FIRST #{limit} ROWS ONLY
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .OFFSET_ROWS("#{offset}")
    .FETCH_FIRST_ROWS_ONLY("#{limit}")
    .toString();
}

SqlBuilder 和 SelectBuilder (已經廢棄)

在版本 3.2 之前,我們的實現方式不太一樣,我們利用 ThreadLocal 變數來掩蓋一些對 Java DSL 不太友好的語言限制。現在,現代 SQL 構建框架使用的構建器和匿名內部類思想已被人們所熟知。因此,我們廢棄了基於這種實現方式的 SelectBuilder 和 SqlBuilder 類。

下面的方法僅僅適用於廢棄的 SqlBuilder 和 SelectBuilder 類。

方法 描述
BEGIN() / RESET() 這些方法清空 SelectBuilder 類的 ThreadLocal 狀態,並準備好構建一個新的語句。開始新的語句時,BEGIN() 是最名副其實的(可讀性最好的)。但如果由於一些原因(比如程式邏輯在某些條件下需要一個完全不同的語句),在執行過程中要重置語句構建狀態,就很適合使用 RESET()
SQL() 該方法返回生成的 SQL() 並重置 SelectBuilder 狀態(等價於呼叫了 BEGIN()RESET())。因此,該方法只能被呼叫一次!

SelectBuilder 和 SqlBuilder 類並不神奇,但最好還是知道它們的工作原理。 SelectBuilder 以及 SqlBuilder 藉助靜態匯入和 ThreadLocal 變數實現了對插入條件友好的簡潔語法。要使用它們,只需要靜態匯入這個類的方法即可,就像這樣(只能使用其中的一條,不能同時使用):

import static org.apache.ibatis.jdbc.SelectBuilder.*;
import static org.apache.ibatis.jdbc.SqlBuilder.*;

然後就可以像下面這樣建立一些方法:

/* 已被廢棄 */
public String selectBlogsSql() {
  BEGIN(); // 重置 ThreadLocal 狀態變數
  SELECT("*");
  FROM("BLOG");
  return SQL();
}
        
/* 已被廢棄 */
private String selectPersonSql() {
  BEGIN(); // 重置 ThreadLocal 狀態變數
  SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
  SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
  FROM("PERSON P");
  FROM("ACCOUNT A");
  INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
  INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
  WHERE("P.ID = A.ID");
  WHERE("P.FIRST_NAME like ?");
  OR();
  WHERE("P.LAST_NAME like ?");
  GROUP_BY("P.ID");
  HAVING("P.LAST_NAME like ?");
  OR();
  HAVING("P.FIRST_NAME like ?");
  ORDER_BY("P.ID");
  ORDER_BY("P.FULL_NAME");
  return SQL();
}

日誌

Mybatis 通過使用內建的日誌工廠提供日誌功能。內建日誌工廠將會把日誌工作委託給下面的實現之一:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

MyBatis 內建日誌工廠會基於執行時檢測資訊選擇日誌委託實現。它會(按上面羅列的順序)使用第一個查詢到的實現。當沒有找到這些實現時,將會禁用日誌功能。

不少應用伺服器(如 Tomcat 和 WebShpere)的類路徑中已經包含 Commons Logging。注意,在這種配置環境下,MyBatis 會把 Commons Logging 作為日誌工具。這就意味著在諸如 WebSphere 的環境中,由於提供了 Commons Logging 的私有實現,你的 Log4J 配置將被忽略。這個時候你就會感覺很鬱悶:看起來 MyBatis 將你的 Log4J 配置忽略掉了(其實是因為在這種配置環境下,MyBatis 使用了 Commons Logging 作為日誌實現)。如果你的應用部署在一個類路徑已經包含 Commons Logging 的環境中,而你又想使用其它日誌實現,你可以通過在 MyBatis 配置檔案 mybatis-config.xml 裡面新增一項 setting 來選擇其它日誌實現。

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>
      

可選的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是實現了 org.apache.ibatis.logging.Log 介面,且構造方法以字串為引數的類完全限定名。

你也可以呼叫以下任一方法來選擇日誌實現:

org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();

你應該在呼叫其它 MyBatis 方法之前呼叫以上的某個方法。另外,僅當執行時類路徑中存在該日誌實現時,日誌實現的切換才會生效。如果你的環境中並不存在 Log4J,你卻試圖呼叫了相應的方法,MyBatis 就會忽略這一切換請求,並將以預設的查詢順序決定使用的日誌實現。

關於 SLF4J、Apache Commons Logging、Apache Log4J 和 JDK Logging 的 API 介紹不在本文件介紹範圍內。不過,下面的例子可以作為一個快速入門。有關這些日誌框架的更多資訊,可以參考以下連結:

日誌配置

你可以通過在包、對映類的全限定名、名稱空間或全限定語句名上開啟日誌功能,來檢視 MyBatis 的日誌語句。

再次提醒,具體配置步驟取決於日誌實現。接下來我們會以 Log4J 作為示範。配置日誌功能非常簡單:新增一個或多個配置檔案(如 log4j.properties),有時還需要新增 jar 包(如 log4j.jar)。下面的例子將使用 Log4J 來配置完整的日誌服務。一共兩個步驟:

步驟 1:新增 Log4J 的 jar 包

由於我們使用的是 Log4J,我們要確保它的 jar 包可以被應用使用。為此,需要將 jar 包新增到應用的類路徑中。Log4J 的 jar 包可以在上面的連結中下載。

對於 web 應用或企業級應用,你可以將 log4j.jar 新增到 WEB-INF/lib 目錄下;對於獨立應用,可以將它新增到 JVM 的 -classpath 啟動引數中。

步驟 2:配置 Log4J

配置 Log4J 比較簡單。假設你需要記錄這個對映器的日誌:

package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

在應用的類路徑中建立一個名為 log4j.properties 的檔案,檔案的具體內容如下:

# 全域性日誌配置
log4j.rootLogger=ERROR, stdout
# MyBatis 日誌配置
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# 控制檯輸出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

上述配置將使 Log4J 詳細列印 org.mybatis.example.BlogMapper 的日誌,對於應用的其它部分,只打印錯誤資訊。

為了實現更細粒度的日誌輸出,你也可以只打印特定語句的日誌。以下配置將只打印語句 selectBlog 的日誌:

log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE

或者,你也可以列印一組對映器的日誌,只需要開啟對映器所在的包的日誌功能即可:

log4j.logger.org.mybatis.example=TRACE

某些查詢可能會返回龐大的結果集。這時,你可能只想檢視 SQL 語句,而忽略返回的結果集。為此,SQL 語句將會在 DEBUG 日誌級別下記錄(JDK 日誌則為 FINE)。返回的結果集則會在 TRACE 日誌級別下記錄(JDK 日誌則為 FINER)。因此,只要將日誌級別調整為 DEBUG 即可:

log4j.logger.org.mybatis.example=DEBUG

但如果你要為下面的對映器 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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

這時,你可以通過開啟名稱空間的日誌功能來對整個 XML 記錄日誌:

log4j.logger.org.mybatis.example.BlogMapper=TRACE

而要記錄具體語句的日誌,可以這樣做:

log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE

你應該會發現,為對映器和 XML 檔案開啟日誌功能的語句毫無差別。

提示 如果你使用的是 SLF4J 或 Log4j 2,MyBatis 會設定 tag 為 MYBATIS。

配置檔案 log4j.properties 的餘下內容用來配置輸出器(appender),這一內容已經超出本文件的範圍。關於 Log4J 的更多內容,可以參考上面的 Log4J 網站。或者,你也可以簡單地做個實驗,看看不同的配置會產生怎樣的效果。