> 文檔出自:[https://mybatis.org/mybatis-3/zh/sqlmap-xml.html](https://mybatis.org/mybatis-3/zh/sqlmap-xml.html)
## Mybatis專題二:映射文件
[TOC]
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">
~~~
Select 元素的屬性
| 屬性 | 描述 |
| --- | --- |
| 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">
~~~
Insert, Update, Delete 元素的屬性
| 屬性 | 描述 |
| --- | --- |
| 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">
~~~
selectKey 元素的屬性
| 屬性 | 描述 |
| --- | --- |
| keyProperty | selectKey語句結果應該被設置到的目標屬性。如果生成列不止一個,可以用逗號分隔多個屬性名稱。 |
| keyColumn | 返回結果集中生成列屬性的列名。如果生成列不止一個,可以用逗號分隔多個屬性名稱。 |
| resultType | 結果的類型。通常 MyBatis 可以推斷出來,但是為了更加準確,寫上也不會有什么問題。MyBatis 允許將任何簡單類型用作主鍵的類型,包括字符串。如果生成列不止一個,則可以使用包含期望屬性的 Object 或 Map。 |
| order | 可以設置為BEFORE或AFTER。如果設置為BEFORE,那么它首先會生成主鍵,設置keyProperty再執行插入語句。如果設置為AFTER,那么先執行插入語句,然后是selectKey中的語句 - 這和 Oracle 數據庫的行為相似,在插入語句內部可能有嵌入索引調用。 |
| statementType | 和前面一樣,MyBatis 支持STATEMENT,PREPARED和CALLABLE類型的映射語句,分別代表Statement,PreparedStatement和CallableStatement類型。 |
### 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,這個參數可以隨意命名。原始類型或簡單數據類型(比如Integer和String)因為沒有其它屬性,會用它們的值來作為參數。 然而,如果傳入一個復雜的對象,行為就會有點不一樣了。比如:
~~~
<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 屬性允許你指定IN,OUT或INOUT參數。如果參數的mode為OUT或INOUT,將會修改參數對象的屬性值,以便作為輸出參數返回。 如果mode為OUT(或INOUT),而且jdbcType為CURSOR(也就是 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", "noone@nowhere.com");
~~~
這種方式也同樣適用于替換表名的情況。
提示用這種方式接受用戶的輸入,并用作語句參數是不安全的,會導致潛在的 SQL 注入攻擊。因此,要么不允許用戶輸入這些字段,要么自行轉義并檢驗這些參數。
### 結果映射
resultMap元素是 MyBatis 中最重要最強大的元素。它可以讓你從 90% 的 JDBCResultSets數據提取代碼中解放出來,并在一些情形下允許你進行一些 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也是一個結果映射,因此具有相同的結構和元素;或者引用其它的結果映射
ResultMap 的屬性列表
| 屬性 | 描述 |
| --- | --- |
| id | 當前命名空間中的一個唯一標識,用于標識一個結果映射。 |
| type | 類的完全限定名, 或者一個類型別名(關于內置的類型別名,可以參考上面的表格)。 |
| autoMapping | 如果設置這個屬性,MyBatis 將會為本結果映射開啟或者關閉自動映射。 這個屬性會覆蓋全局的屬性 autoMappingBehavior。默認值:未設置(unset)。 |
最佳實踐最好逐步建立結果映射。單元測試可以在這個過程中起到很大幫助。 如果你嘗試一次性創建像上面示例那么巨大的結果映射,不僅容易出錯,難度也會直線上升。 所以,從最簡單的形態開始,逐步迭代。而且別忘了單元測試! 有時候,框架的行為像是一個黑盒子(無論是否開源)。因此,為了確保實現的行為與你的期望相一致,最好編寫單元測試。 并且單元測試在提交 bug 時也能起到很大的作用。
下一部分將詳細說明每個元素。
#### id & result
~~~
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
~~~
這些元素是結果映射的基礎。*id*和*result*元素都將一個列的值映射到一個簡單數據類型(String, int, double, Date 等)的屬性或字段。
這兩者之間的唯一不同是,*id*元素對應的屬性會被標記為對象的標識符,在比較對象實例時使用。 這樣可以提高整體的性能,尤其是進行緩存和嵌套結果映射(也就是連接映射)的時候。
兩個元素都有一些屬性:
Id 和 Result 的屬性
| 屬性 | 描述 |
| --- | --- |
| 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 類型。
BITFLOATCHARTIMESTAMPOTHERUNDEFINEDTINYINTREALVARCHARBINARYBLOBNVARCHARSMALLINTDOUBLELONGVARCHARVARBINARYCLOBNCHARINTEGERNUMERICDATELONGVARBINARYBOOLEANNCLOBBIGINTDECIMALTIMENULLCURSORARRAY
#### 構造方法
通過修改對象屬性的方式,可以滿足大多數的數據傳輸對象(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.String和int的順序給出。
~~~
<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 查詢語句的列名。這會使得prop1和prop2作為參數對象,被設置為對應嵌套 Select 語句的參數。 |
| select | 用于加載復雜類型屬性的映射語句的 ID,它會從 column 屬性指定的列中檢索數據,作為參數傳遞給目標 select 語句。 具體請參考下面的例子。注意:在使用復合主鍵的時候,你可以使用column="{prop1=col1,prop2=col2}"這樣的語法來指定多個傳遞給嵌套 Select 查詢語句的列名。這會使得prop1和prop2作為參數對象,被設置為對應嵌套 Select 語句的參數。 |
| fetchType | 可選的。有效值為lazy和eager。 指定屬性后,將在映射中忽略全局配置參數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。注意,本屬性對外部的結果映射無效,所以不能搭配select或resultMap元素使用。默認值:未設置(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 出現的列,如果沒有設置手動映射,將被自動映射。在自動映射處理完畢后,再處理手動映射。 在下面的例子中,*id*和*userName*列將被自動映射,*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>
~~~
在該結果映射中,*Blog*和*Author*均將被自動映射。但是注意*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}),以便替換成在[配置文件屬性](https://mybatis.org/mybatis-3/zh/configuration.html#properties)中定義的值。
從版本 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"/>
~~~
- JavaCook
- Java專題零:類的繼承
- Java專題一:數據類型
- Java專題二:相等與比較
- Java專題三:集合
- Java專題四:異常
- Java專題五:遍歷與迭代
- Java專題六:運算符
- Java專題七:正則表達式
- Java專題八:泛型
- Java專題九:反射
- Java專題九(1):反射
- Java專題九(2):動態代理
- Java專題十:日期與時間
- Java專題十一:IO與NIO
- Java專題十一(1):IO
- Java專題十一(2):NIO
- Java專題十二:網絡
- Java專題十三:并發編程
- Java專題十三(1):線程與線程池
- Java專題十三(2):線程安全與同步
- Java專題十三(3):內存模型、volatile、ThreadLocal
- Java專題十四:JDBC
- Java專題十五:日志
- Java專題十六:定時任務
- Java專題十七:JavaMail
- Java專題十八:注解
- Java專題十九:淺拷貝與深拷貝
- Java專題二十:設計模式
- Java專題二十一:序列化與反序列化
- 附加專題一:MySQL
- MySQL專題零:簡介
- MySQL專題一:安裝與連接
- MySQL專題二:DDL與DML語法
- MySQL專題三:工作原理
- MySQL專題四:InnoDB存儲引擎
- MySQL專題五:sql優化
- MySQL專題六:數據類型
- 附加專題二:Mybatis
- Mybatis專題零:簡介
- Mybatis專題一:配置文件
- Mybatis專題二:映射文件
- Mybatis專題三:動態SQL
- Mybatis專題四:源碼解析
- 附加專題三:Web編程
- Web專題零:HTTP協議
- Web專題一:Servlet
- Web專題二:Cookie與Session
- 附加專題四:Redis
- Redis專題一:數據類型
- Redis專題二:事務
- Redis專題三:key的過期
- Redis專題四:消息隊列
- Redis專題五:持久化