前面我們講解了一下關於MyBatis的動態SQL查詢,對MyBatis的基本用法有了大致的瞭解,如果忘記了可以去複習一下,MyBatis學習——動態SQL。今天我們將講解一下MyBatis的高級查詢知識點。
前言:在關係型數據庫中,我們經常要處理一對一 、一對多的關係。在面對這種關係的時候,我們可能要寫多個方法分別查詢這些數據,然後再組合到一起。這種處理方式特別適合用在大型系統上,由於分庫分表,這種用法可以減少表之間的關聯查詢,方便系統進行擴展。但是在一般的企業級應用中,使用MyBatis的高級結果映射便可以輕鬆地處理這種一對一 、 一對多的關係。
比如,現在我有兩張表,一張表示用戶相關信息,另一張表示用戶角色相關信息,一個用戶可以對應多個角色。
用戶表:
角色表:
下面的內容我們將根據這兩張表用戶與角色的內容進行講解。
一、一對一映射
1、使用自動映射處理一對一關係(對象與數據庫字段映射)
比如,這裏我們使用了role表的查詢,並關聯user表,將我們需要的結果全部都查詢出來,然後再在對應的實體中進行映射,所以在User實體當中,我們對role進行映射。查詢如下:
<select id="selectUserAndRoleById" resultType="User">
select
u.*,
r.id "role.id",
r.role "role.role",
r.enabled "role.enabled"
from user u
inner join role r on u.id = r.user_id
where u.id = #{id}
</select>
我們在User實體中對Role進行映射。如下:
當我們查詢一對一的關聯數據時,就可以自動映射到role實體上去。MyBatis還支持複雜的屬性映射 , 可以多層嵌套。 例如,將 role.role 映射到 role.role上。 MyBatis 會先查找role屬性,如果存在role屬性就創建 role對象, 然後在role對象中繼續查找role,將role的值綁定到role對象的role屬性上。
2、使用resultMap配置一對一映射
在上面我們直接通過在實體中進行了映射,除此之外,我們還可以在resultMap中進行配置,對多個不同的屬性進行配置。
<resultMap id="userMap" type="net.anumbrella.mybatis.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="expried" column="expried"/>
<result property="disabled" column="disabled"/>
<result property="role.id" column="role.id"/>
<result property="role.role" column="role.role"/>
<result property="role.enabled" column="role.enabled"/>
</resultMap>
同時因爲MyBatis是支持resultMap映射繼承的, 因此要先簡化上面的resultMap配置。我們可以把角色抽出來單獨作爲一個resultMap,然後繼承(在resultMap中使用extends標籤繼承)用戶屬性的resultMap即可。由於這個內容比較簡單,這裏就不過多敘述。
3、使用resultMap的association標籤配置一對一映射
在resultMap中,assocation標籤用於和一個複雜的類型進行關聯, 即用於一對一的關聯配置。
assocation標籤包含以下屬性:
- property:對應實體類中的屬性名,必填項。
- javaType:屬性對應的Java類型。
- resultMap:可以直接使用現有的resultMap,而不需要在這裏配置。
- columPrefix:查詢列的前綴,配置前綴後,在子標籤result的colum時可以省略前綴。
因此上面的resultMap可以更改如下:
<resultMap id="userRoleMap" extends="userMap" type="net.anumbrella.mybatis.entity.User">
<association property="role" columnPrefix="role_" javaType="net.anumbrella.mybatis.entity.Role">
<result property="role.id" column="role.id"/>
<result property="role.role" column="role.role"/>
<result property="role.enabled" column="role.enabled"/>
</association>
</resultMap>
注意:這裏是帶表前綴的,所以在查出的結果對應列中需要加入role_前綴。
同時我們還可以進一步精簡,如下:
<resultMap id="roleMap" type="net.anumbrella.mybatis.entity.Role">
<id property="id" column="id"/>
<result property="role" column="role"/>
<result property="enabled" column="enabled"/>
</resultMap>
<resultMap id="userRoleMap" extends="userMap" type="net.anumbrella.mybatis.entity.User">
<association property="role" columnPrefix="role_" resultMap="net.anumbrella.mybatis.dao.RoleDao.roleMap"/>
</resultMap>
4、association標籤的嵌套查詢
association標籤的嵌套查詢常用的屬性如下:
- select:另一個映射查詢的id, MyBatis會額外執行這個查詢獲取嵌套對象的結果。
- column:列名(或別名),將主查詢中列的結果作爲嵌套查詢的參數,配置方式如column={propl=coll,prop2=col2}, propl和prop2將作爲嵌套查詢的參數。
- fetchType:數據加載方式,可選值爲lazy和eager ,分別爲延遲加載和積極加載,這個配置會覆蓋全局的 lazyLoadingEnabled配置。
比如下面的查詢結果配置,同時我們也在select中配置了子查詢。
<resultMap id="userRoleMapSelect" extends="userMap" type="net.anumbrella.mybatis.entity.User">
<association property="role"
fetchType="lazy"
select="net.anumbrella.mybatis.dao.RoleDao.selectRoleById"
column="{id=role.id}"/>
</resultMap>
子查詢在RoleDao.xml配置,如下:
<select id="selectRoleById" resultMap="roleMap">
select * from role where id = #{id}
</select>
因爲第一個SQL的查詢結果只有一條,role.id關聯了另一個查詢,因此執行了兩次SQL。
如果查詢的不是1條數據,而是N條數據,那就會出現N+1問題,主SQL會查詢一次,查詢出N條結果, 這N條結果要各自執行一次查詢,那就需要進行N次查詢。 如何解決這個問題呢?
在上面介紹association標籤的屬性時, 介紹了fetchType數據加載方式, 這個方式可以幫我們實現延遲加載,解決N+1的問題。
在 MyBatis 的全局配置中, 有一個參數爲 aggressiveLazyLoading 。這個參數的含義是,當該參數設置爲 true時,對任意延遲屬性的調用會使帶有延遲加載屬性的對象完整加載,反之, 每種屬性都將按需加載。
因此當我們使用MyBatis懶加載時,需要配置aggressiveLazyLoading爲false。如下:
<configuration>
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
</configuration>
按照上面的介紹,需要把fetchType設置爲lazy(這裏設置lazy,相當於把lazyLoadingEnabled設置爲true),這樣設置後,只有當調用getRole()方法獲取role的時候, MyBatis纔會執行嵌套查詢去獲取數據。
但是有些時候還是需要在觸發某方法時將所有的數據都加載進來 , 而我們己經將aggressiveLazyLoading設置爲 false ,這種情況又該怎麼解決呢?
MyBatis 仍然提供了參數 lazyLoadTriggerMethods 幫助解決這個問題, 這個參數的含義是, 當調用配置中的方法時, 加載全部的延遲加載數據。 默認值爲” equals , clone ,hashCode ,toString ” 。
二、一對多映射
1、collection集合的嵌套結果映射
在前文我們介紹一對一的映射關係中,有多種實現方式,但是一對多的實現方式只有一種方式————collection。
和association類似集合的嵌套結果映射就是指通過一次SQL查詢將所有的結果查詢出來, 然後通過配置的結果映射, 將數據映射到不同的對象中去。
比如,我們在User實體中添加roleList對象,同時添加set/get方法。
public class User implements Serializable{
private Long id;
private String username;
private String password;
private Integer expired;
private Integer disabled;
private String email;
private Role role;
private List<Role> roleList;
// set/get方法
...
}
然後我們添加關聯查詢,如下:
<resultMap id="userRoleListMap" extends="userMap" type="net.anumbrella.mybatis.entity.User">
<collection property="roleList" columnPrefix="role_"
resultMap="net.anumbrella.mybatis.dao.RoleDao.roleMap"/>
</resultMap>
<select id="selectAllUserAndRoles" resultMap="userRoleListMap">
select
u.*,
r.id "role_id",
r.role "role_role",
r.enabled "role_enabled"
from user u
inner join role r on u.id = r.user_id
</select>
接着在UserDao中添加selectAllUserAndRoles方法。
SQL執行的結果數有 3 條, 後面輸出的用戶數是 2 , 本來查詢出的3條結果經過MyBatis對collection數據的處理後,變成了兩條。
因爲這裏使用了collection,它會把集合的數據合併。那麼MyBatis的合併規則如何呢?
MyBatis 判斷結果是否相同時, 最簡單的情況就是在映射配置中 至少有一個id標籤 , 在userMap中配置如下。
<id property =” id ” column=” id ” />
我們對 id (構造方法中爲 idArg )的理解一般是 , 它配置的字段爲表的主鍵(聯合主鍵時可以配置多個 id 標籤), 因爲MyBatis的 resultMap 只用於配置結果如何映射 , 並不知道這個表具體如何。 id 的唯一作用就是 在嵌套的映射配置時判斷數據是否相同, 當配置 id 標籤時, MyBatis只需要逐條比較所有數據中 id 標籤配置的字段值是否相同即可。 在配置嵌套結果查詢時 , 配置 id 標籤可以提高處理效率。
因爲前兩條數據的userMap部分的 id 相同 , 所以它們屬於同一個用戶, 因此這條數據會合併到同一個用戶中。
雖然 association 和 collection 標籤是分開介紹的 , 但是這兩者可以組合使用或者互相嵌套使用 , 也可以使用 符合自己需要的任何數據結 構, 不需要侷限於數據庫表之間的關聯關係 。
2、collection集合的嵌套查詢
集合的嵌套查詢也association類似,這裏不過多講述,這是大致提示一下。
同樣的我們添加相關查詢,如下:
<resultMap id="userRoleListMapSelect" extends="userMap" type="net.anumbrella.mybatis.entity.User">
<collection property="roleList"
fetchType="lazy"
select="net.anumbrella.mybatis.dao.RoleDao.selectRoleByUserId"
column="{userId=id}"/>
</resultMap>
<select id="selectAllUserAndRolesSelect" resultMap="userRoleListMapSelect">
select
u.id,
u.username,
u.password,
u.email,
u.expired,
u.disabled
from user u
where u.id = #{id}
</select>
同理,我們在RoleDao.xml中配置selectRoleByUserId的查詢,如下:
<select id="selectRoleByUserId" resultMap="roleMap">
select * from role where user_id = #{userId}
</select>
同理因爲所有嵌套查詢都 配置爲延遲加載 , 因此不存在 N+1 的問題 。
三、鑑別器映射
有時一個單獨的數據庫查詢會返回很多不同數據類型(希望有些關聯)的結果集。
鑑別器標籤就是用來處理這種情況的。
discriminator標籤常用的兩個屬性:
- column:該屬性用於設置要進行鑑別比較值的列。
- javaType:該屬性用於指定列的類型, 保證使用相同的Java類型來比較值。
discriminator標籤可以有1個或多個case標籤,case標籤包含以下三個屬性:
- value:該值爲discriminator指定column用來匹配的值。
- resultMap:當column的值和value的值匹配時,可以配置使用resultMap指定的映射, resultMap優先級高於resultType。
- resultType:當column的值和value的值匹配時,用於配置使用resultType指定的映射。
<resultMap id="rolePrivilegeListMapChoose" type="net.anumbrella.mybatis.entity.Role">
<discriminator column="enabled" javaType="int">
<case value="1" resultMap="rolePrivilegeListMapSelect"/>
<case value="0" resultMap="roleMap"/>
</discriminator>
</resultMap>
比如,像上面的enabled爲不同的值時會根據情況返回不同的resultMap作爲結果。
在discriminator中也可以使用javaType返回屬性。
<resultMap id="rolePrivilegeListMapChoose" type="net.anumbrella.mybatis.entity.Role">
<discriminator column="enabled" javaType="int">
<case value="1" resultMap="rolePrivilegeListMapSelect"/>
<case value="0" resultMap="roleMap"/>
<case value="0" resultType="net.anumbrella.mybatis.entity.Role>
<id property ="id" column=“id” />
<result property ="role" column="role" />
</case>
</discriminator>
</resultMap>
鑑別器是一種很少使用的方式 , 在使用前一定要完全掌握, 沒有把握 的情況下要儘可能避免使用。
關於MyBatis的高級結果映射的介紹到此爲止,更多詳細的用法可以去查閱相關文檔。
上面的代碼示例:mybatis-demo
參考
- 《MyBatis從入門到精通》