MyBatis之xml 映射文件詳解
MyBatis 的真正強大在於它的映射語句,SQL 映射文件只有很少的幾個頂級元素(按照應被定義的順序列出):
- cache – 對給定命名空間的緩存配置。
- cache-ref – 對其他命名空間緩存配置的引用。
- resultMap – 是最複雜也是最強大的元素,用來描述如何從數據庫結果集中來加載對象。
parameterMap – 已被廢棄!老式風格的參數映射。更好的辦法是使用內聯參數,此元素可能在將來被移除。文檔中不會介紹此元素。- sql – 可被其他語句引用的可重用語句塊。
- insert – 映射插入語句
- update – 映射更新語句
- delete – 映射刪除語句
- select – 映射查詢語句
1 Sql標籤
1.1 select
查詢語句是 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) |
resultType | 從這條語句中返回的期望類型的類的完全限定名或別名。 注意如果返回的是集合,那應該設置爲集合包含的類型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同時使用。 |
resultMap | 外部 resultMap 的命名引用。結果集的映射是 MyBatis 最強大的特性,如果你對其理解透徹,許多複雜映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同時使用。 |
flushCache | 將其設置爲 true 後,只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值:false。 |
useCache | 將其設置爲 true 後,將會導致本條語句的結果被二級緩存緩存起來,默認值:對 select 元素爲 true。 |
timeout | 將其設置爲 true 後,將會導致本條語句的結果被二級緩存緩存起來,默認值:對 select 元素爲 true。 |
useCache | 這個設置是在拋出異常之前,驅動程序等待數據庫返回請求結果的秒數。默認值爲未設置(unset)(依賴驅動)。 |
fetchSize | 這是一個給驅動的提示,嘗試讓驅動程序每次批量返回的結果行數和這個設置值相等。 默認值爲未設置(unset)(依賴驅動)。 |
useCache | 將其設置爲 true 後,將會導致本條語句的結果被二級緩存緩存起來,默認值:對 select 元素爲 true。 |
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 | 這個設置僅對多結果集的情況適用。它將列出語句執行後返回的結果集並給每個結果集一個名稱,名稱是逗號分隔的。 |
1.2 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 可以通過類型處理器推斷出具體傳入語句的參數,默認值爲未設置(unset)。 |
flushCache | 將其設置爲 true 後,只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值:true(對於 insert、update 和 delete 語句)。 |
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>
每一個在包 domain.blog 中的 Java Bean,在沒有註解的情況下,會使用 Bean 的首字母小寫的非限定類名來作爲它的別名。 比如 domain.blog.Author 的別名爲 author;若有註解,則別名爲其註解值。見下面的例子:
@Alias("author")
public class Author {
...
}
對於不支持自動生成類型的數據庫或可能不支持自動生成主鍵的 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 語句的映射類型,分別代表 PreparedStatement 和 CallableStatement 類型。 |
1.3 sql
這個元素可以被用來定義可重用的 SQL 代碼段,這些 SQL 代碼可以被包含在其他語句中。它可以(在加載的時候)被靜態地設置參數。 在不同的包含語句中可以設置不同的值到參數佔位符上。比如:
<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 屬性裏或 include 元素的內部語句中,例如:
<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>
2 參數
之前見到的所有語句中,使用的都是簡單參數。實際上參數是 MyBatis 非常強大的元素。對於簡單的使用場景,大約 90% 的情況下都不需要使用複雜的參數,比如:
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>
上面的這個示例說明了一個非常簡單的命名參數映射。參數類型被設置爲 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 Type。閱讀 PreparedStatement.setNull()的 JavaDoc 文檔來獲取更多信息。
要更進一步地自定義類型處理方式,你也可以指定一個特殊的類型處理器類(或別名),比如:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
儘管看起來配置變得越來越繁瑣,但實際上,很少需要如此繁瑣的配置。
對於數值類型,還有一個小數保留位數的設置,來指定小數點後保留的位數。
#{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}
儘管所有這些選項很強大,但大多時候你只須簡單地指定屬性名,其他的事情 MyBatis 會自己去推斷,頂多要爲可能爲空的列指定 jdbcType。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
2.1 字符串替換
默認情況下,使用 #{} 格式的語法會導致 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);
// and more "findByXxx" method
可以只寫這樣一個方法:
@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注入攻擊,因此要麼不允許用戶輸入這些字段,要麼自行轉義並檢驗。
3 結果映射
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 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>