【MyBatis-3】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)
parameterMap 這是引用外部 parameterMap 的已經被廢棄的方法。請使用內聯參數映射和 parameterType 屬性。
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)。
parameterMap 這是引用外部 parameterMap 的已經被廢棄的方法。請使用內聯參數映射和 parameterType 屬性。
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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章