MyBatis真正的力量是在映射語句中。這里是奇跡發生的地方。對於所有的力量,SQL映射的XML文件是相當的簡單。當然如果你將它們和對等功能的JDBC代碼來比較,你會發現映射文件節省了大約95%的代碼量。MyBatis的構建就是聚焦於SQL的,使其遠離於普通的方式。
SQL映射文件有很少的幾個頂級元素(按照它們應該被定義的順序):
cache - 配置給定命名空間的緩存。
cache-ref – 從其他命名空間引用緩存配置。
resultMap – 最復雜,也是最有力量的元素,用來描述如何從數據庫結果集中來加載你的對象。
parameterMap – 已經被廢棄了!老式風格的參數映射。內聯參數是首選,這個元素可能在將來被移除。這里不會記錄。
sql – 可以重用的SQL塊,也可以被其他語句引用。
insert – 映射插入語句
update – 映射更新語句
delete – 映射刪除語句
select – 映射查詢語句
select
查詢語句是使用MyBatis時最常用的元素之一。直到你從數據庫取出數據時才會發現將數據存在數據庫中是多么的有價值,所以許多應用程序查詢要比更改數據多的多。對於每次插入,更新或刪除,那也會有很多的查詢。這是MyBatis的一個基本原則,也是將重心和努力放到查詢和結果映射的原因。對簡單類別的查詢元素是非常簡單的。比如:
<select id=”selectPerson” parameterType=”int” resultType=”hashmap”>
SELECT * FROM PERSON WHERE ID = #{id}
</select>
這個語句被稱作selectPerson,使用一個int(或Integer)類型的參數,並返回一個HashMap類型的對象,其中的鍵是列名,值是列對應的值。
注意參數注釋:
#{id}
這就告訴MyBatis創建一個預處理語句參數。使用JDBC,這樣的一個參數在SQL中會由一個“?”來標識,並被傳遞到一個新的預處理語句中,就像這樣:
// Similar JDBC code, NOT 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=”10000”
fetchSize=”256”
statementType=”PREPARED”
resultSetType=”FORWARD_ONLY”
>
| 屬性 |
描述 |
| id |
在命名空間中唯一的標識符,可以被用來引用這條語句。 |
| parameterType |
將會傳入這條語句的參數類的完全限定名或別名。 |
| parameterMap |
這是引用外部parameterMap的已經被廢棄的方法。使用內聯參數映射和parameterType屬性。 |
| resultType |
從這條語句中返回的期望類型的類的完全限定名或別名。注意集合情形,那應該是集合可以包含的類型,而不能是集合本身。使用resultType或resultMap,但不能同時使用。 |
| resultMap |
命名引用外部的resultMap。返回map是MyBatis最具力量的特性,對其有一個很好的理解的話,許多復雜映射的情形就能被解決了。使用resultMap或resultType,但不能同時使用。 |
| flushCache |
將其設置為true,不論語句什么時候被帶哦用,都會導致緩存被清空。默認值:false。 |
| useCache | 將其設置為true,將會導致本條語句的結果被緩存。默認值:true。 |
| timeout |
這個設置驅動程序等待數據庫返回請求結果,並拋出異常時間的最大等待值。默認不設置(驅動自行處理)。 |
| fetchSize |
這是暗示驅動程序每次批量返回的結果行數。默認不設置(驅動自行處理)。 |
| statementType |
STATEMENT,PREPARED或CALLABLE的一種。這會讓MyBatis使用選擇使用Statement,PreparedStatement或CallableStatement。默認值:PREPARED。 |
| resultSetType |
FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE中的一種。默認是不設置(驅動自行處理)。 |
insert,update,delete
數據變更語句insert,update和delete在它們的實現中非常相似:
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
useGeneratedKeys=""
timeout="20000">
<update
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20000">
<delete
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20000">
| 屬性 |
描述 |
| id | 在命名空間中唯一的標識符,可以被用來引用這條語句。 |
| parameterType | 將會傳入這條語句的參數類的完全限定名或別名。 |
| parameterMap | 這是引用外部parameterMap的已經被廢棄的方法。使用內聯參數映射和parameterType屬性。 |
| flushCache | 將其設置為true,不論語句什么時候被帶哦用,都會導致緩存被清空。默認值:false。 |
| timeout | 這個設置驅動程序等待數據庫返回請求結果,並拋出異常時間的最大等待值。默認不設置(驅動自行處理)。 |
| statementType | STATEMENT,PREPARED或CALLABLE的一種。這會讓MyBatis使用選擇使用Statement,PreparedStatement或CallableStatement。默認值:PREPARED。 |
| useGeneratedKeys | (僅對insert有用)這會告訴MyBatis使用JDBC的getGeneratedKeys方法來取出由數據(比如:像MySQL和SQL Server這樣的數據庫管理系統的自動遞增字段)內部生成的主鍵。默認值:false。 |
| keyProperty | (僅對insert有用)標記一個屬性,MyBatis會通過getGeneratedKeys或者通過insert語句的selectKey子元素設置它的值。默認:不設置。 |
下面就是insert,update和delete語句的示例:
<insert id="insertAuthor" parameterType="domain.blog.Author">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateAuthor" parameterType="domain.blog.Author">
update Author set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>
<delete id="deleteAuthor” parameterType="int">
delete from Author where id = #{id}
</delete>
如前所述,插入語句有一點多,它有一些屬性和子元素用來處理主鍵的生成。
首先,如果你的數據庫支持自動生成主鍵的字段(比如MySQL和SQL Server),那么你可以設置useGeneratedKeys=”true”,而且設置keyProperty到你已經做好的目標屬性上。例如,如果上面的Author表已經對id使用了自動生成的列類型,那么語句可以修改為:
<insert id="insertAuthor" parameterType="domain.blog.Author"
useGeneratedKeys=”true” keyProperty=”id”>
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
MyBatis有另外一種方法來處理數據庫不支持自動生成類型,或者可能JDBC驅動不支持自動生成主鍵時的主鍵生成問題。
這里有一個簡單(甚至很傻)的示例,它可以生成一個隨機ID(可能你不會這么做,但是這展示了MyBatis處理問題的靈活性,因為它並不真的關心ID的生成):
<insert id="insertAuthor" parameterType="domain.blog.Author">
<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">
sql
這個元素可以被用來定義可重用的SQL代碼段,可以包含在其他語句中。比如:
<sql id=”userColumns”> id,username,password </sql>
這個SQL片段可以被包含在其他語句中,例如:
<select id=”selectUsers” parameterType=”int” resultType=”hashmap”>
select <include refid=”userColumns”/>
from some_table
where id = #{id}
</select>
Parameters
在之前的語句中,你已經看到了一些簡單參數的示例。在MyBatis中參數是非常強大的元素。對於簡單的做法,大概90%的情況,是不用太多的,比如:
<select id=”selectUsers” parameterType=”int” resultType=”User”>
select id, username, password
from users
where id = #{id}
</select>
上面的這個示例說明了一個非常簡單的命名參數映射。參數類型被設置為“int”,因此這個參數可以被設置成任何內容。原生的類型或簡單數據類型,比如整型和沒有相關屬性的字符串,因此它會完全用參數來替代。然而,如果你傳遞了一個復雜的對象,那么MyBatis的處理方式就會有一點不同。比如:
<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應該被確定來保證使用正確類型處理器。
注意:如果null被當作值來傳遞,對於所有可能為空的列,JDBC Type是需要的。以可以自己通過閱讀預處理語句的setNull()方法的JavaDocs文檔來研究這個。
為了自定義類型處理器,你可以指定一個確定的類型處理器類(或別名),比如:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
盡管它看起來繁瑣,但是實際上是你很少設置它們其中之一。
對於數值類型,對於決定有多少數字是相關的,有一個數值范圍。
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
最后,mode屬性允許你指定IN,OUT或INOUT參數。如果參數為OUT或INOUT,參數對象屬性的真實值將會被改變,就像你期望你需要你個輸出參數。如果mode為OUT(或INOUT),而且jdbcType為CURSOR(也就是Oracle的REFCURSOR),你必須指定一個resultMap來映射結果集到參數類型。要注意這里的javaType屬性是可選的,如果左邊的空白是jdbcType的CURSOR類型,它會自動地被設置為結果集。
#{department,
mode=OUT,
jdbcType=CURSOR,
javaType=ResultSet,
resultMap=departmentResultMap}
MyBatis也支持很多高級的數據類型,比如結構體,但是當注冊out參數時你必須告訴語句類型名稱。比如(再次提示,在實際中不要像這樣換行):
#{middleInitial,
mode=OUT,
jdbcType=STRUCT,
jdbcTypeName=MY_TYPE,
resultMap=departmentResultMap}
盡管所有這些強大的選項很多時候你只簡單指定屬性名,MyBatis會自己計算剩余的。最多的情況是你為jdbcType指定可能為空的列名。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
字符串替換
默認情況下,使用#{}格式的語法會導致MyBatis創建預處理語句屬性並以它為背景設置安全的值(比如?)。這樣做很安全,很迅速也是首選做法,有時你只是想直接在SQL語句中插入一個不改變的字符串。比如,像ORDER BY,你可以這樣來使用:
ORDER BY ${columnName}
這里MyBatis不會修改或轉義字符串。
重要:接受從用戶輸出的內容並提供給語句中不變的字符串,這樣做是不安全的。這會導致潛在的SQL注入攻擊,因此你不應該允許用戶輸入這些字段,或者通常自行轉義並檢查。
resultMap
resultMap元素是MyBatis中最重要最強大的元素。它就是讓你遠離90%的需要從結果集中取出數據的JDBC代碼的那個東西,而且在一些情形下允許你做一些JDBC不支持的事情。事實上,編寫相似於對復雜語句聯合映射這些等同的代碼,也許可以跨過上千行的代碼。ResultMap的設計就是簡單語句不需要明確的結果映射,而很多復雜語句確實需要描述它們的關系。
你已經看到簡單映射語句的示例了,但沒有明確的resultMap。比如:
<select id=”selectUsers” parameterType=”int” resultType=”hashmap”>
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
這樣一個語句簡單作用於所有列被自動映射到HashMap的鍵上,這由resultType屬性指定。這在很多情況下是有用的,但是HashMap不能很好描述一個領域模型。那樣你的應用程序將會使用JavaBeans或POJOs(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可以被映射到結果集,就像映射到HashMap一樣簡單。
<select id=”selectUsers” parameterType=”int”
resultType=”com.someapp.model.User”>
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
要記住類型別名是你的伙伴。使用它們你可以不用輸入類的全路徑。比如:
<!-- In Config XML file -->
<typeAlias type=”com.someapp.model.User” alias=”User”/>
<!-- In SQL Mapping XML file -->
<select id=”selectUsers” parameterType=”int”
resultType=”User”>
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
這些情況下,MyBatis會在幕后自動創建一個ResultMap,基於屬性名來映射列到JavaBean的屬性上。如果列名沒有精確匹配,你可以在列名上使用select字句的別名(一個基本的SQL特性)來匹配標簽。比如:
<select id=”selectUsers” parameterType=”int” 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="username"/>
<result property="password" column="password"/>
</resultMap>
引用它的語句使用resultMap屬性就行了(注意我們去掉了resultType屬性)。比如:
<select id=”selectUsers” parameterType=”int”
resultMap=”userResultMap”>
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
如果世界總是這么簡單就好了。
高級結果映射
MyBatis創建的一個想法:數據庫不用永遠是你想要的或需要它們是什么樣的。而我們最喜歡的數據庫最好是第三范式或BCNF模式,但它們有時不是。如果可能有一個單獨的數據庫映射,所有應用程序都可以使用它,這是非常好的,但有時也不是。結果映射就是MyBatis提供處理這個問題的答案。
比如,我們如何映射下面這個語句?
<!-- Very Complex Statement -->
<select id="selectBlogDetails" parameterType="int"
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>
你可能想把它映射到一個智能的對象模型,包含一個作者寫的博客,有很多的博文,每篇博文有零條或多條的評論和標簽。下面是一個完整的復雜結果映射例子(假設作者,博客,博文,評論和標簽都是類型的別名)。我們來看看,但是不用緊張,我們會一步一步來說明。當天最初它看起來令人生畏,但實際上非常簡單。
<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" 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" column="post_author_id"
javaType="Author"/>
<collection property="comments" column="post_id" ofType=" Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" column="post_id" 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 – 一個復雜的類型關聯;許多結果將包成這種類型
嵌入結果映射 – 結果映射自身的關聯,或者參考一個
collection – 復雜類型的集
嵌入結果映射 – 結果映射自身的集,或者參考一個
discriminator – 使用結果值來決定使用哪個結果映射
case – 基於某些值的結果映射
嵌入結果映射 – 這種情形結果也映射它本身,因此可以包含很多相同的元素,或者它可以參照一個外部的結果映射。
最佳實踐:通常逐步建立結果映射。單元測試的真正幫助在這里。如果你嘗試創建一次創建一個向上面示例那樣的巨大的結果映射,那么可能會有錯誤而且很難去控制它來工作。開始簡單一些,一步一步的發展。而且要進行單元測試!使用該框架的缺點是它們有時是黑盒(是否可見源代碼)。你確定你實現想要的行為的最好選擇是編寫單元測試。它也可以你幫助得到提交時的錯誤。
下面一部分將詳細說明每個元素。
id,result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
這些是結果映射最基本內容。id和result都映射一個單獨列的值到簡單數據類型(字符串,整型,雙精度浮點數,日期等)的單獨屬性或字段。
這兩者之間的唯一不同是id表示的結果將是當比較對象實例時用到的標識屬性。這幫助來改進整體表現,特別是緩存和嵌入結果映射(也就是聯合映射)。
每個都有一些屬性:
property
映射到列結果的字段或屬性。如果匹配的是存在的,和給定名稱相同的JavaBeans的屬性,那么就會使用。否則MyBatis將會尋找給定名稱的字段。這兩種情形你可以使用通常點式的復雜屬性導航。比如,你可以這樣映射一些東西:“username”,或者映射到一些復雜的東西:“address.street.number”。
column
從數據庫中得到的列名,或者是列名的重命名標簽。這也是通常和會傳遞給resultSet.getString(columnName)方法參數中相同的字符串。
javaType
一個Java類的完全限定名,或一個類型別名(參加上面內建類型別名的列表)。如果你映射到一個JavaBean,MyBatis通常可以斷定類型。然而,如果你映射到的是HashMap,那么你應該明確地指定javaType來保證所需的行為。
jdbcType
在這個表格之后的所支持的JDBC類型列表中的類型。JDBC類型是僅僅需要對插入,更新和刪除操作可能為空的列進行處理。這是JDBC的需要,而不是MyBatis的。如果你直接使用JDBC編程,你需要指定這個類型-但僅僅對可能為空的值。
typeHandler
我們在前面討論過默認的類型處理器。使用這個屬性,你可以覆蓋默認的類型處理器。這個屬性值是類的完全限定名或者是一個類型處理器的實現,或者是類型別名。
支持的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
構造方法
<constructor>
<idArg column="id" javaType="int"/>
<arg column=”username” javaType=”String”/>
</constructor>
剩余的屬性和規則和固定的id和result元素是相同的。
column
來自數據庫的類名,或重命名的列標簽。這和通常傳遞給resultSet.getString(columnName)方法的字符串是相同的。
javaType
一個Java類的完全限定名,或一個類型別名(參加上面內建類型別名的列表)。如果你映射到一個JavaBean,MyBatis通常可以斷定類型。然而,如果你映射到的是HashMap,那么你應該明確地指定javaType來保證所需的行為。
jdbcType
在這個表格之前的所支持的JDBC類型列表中的類型。JDBC類型是僅僅需要對插入,更新和刪除操作可能為空的列進行處理。這是JDBC的需要,而不是MyBatis的。如果你直接使用JDBC編程,你需要指定這個類型-但僅僅對可能為空的值。
typeHandler
我們在前面討論過默認的類型處理器。使用這個屬性,你可以覆蓋默認的類型處理器。這個屬性值是類的完全限定名或者是一個類型處理器的實現,或者是類型別名。
關聯
