最近開發中遇到了很多樹形結構數據的需要,利用mybatis提供嵌套查詢功能,基本上可以完美解決,但是對於其中的原理並不理解,導致在使用的時候像瞎貓碰死耗子一樣,照着先前成功的例子copy,后來遇到了莫名奇怪的報錯遲遲不能解決,於是百度了一番,大致了解了背后的原理,整理如下。
以簡單的角色-菜單為例
表結構
其中menu為菜單表,role為角色表,roleandmenu是中間表,角色和菜單為多對多的關系,現在我們需要下圖所示的實體類
1 import java.util.List; 2 3 public class RoleInfo { 4 5 private Integer roleid; 6 private String rolename; 7 private List<Menu> menulist; 8 public Integer getRoleid() { 9 return roleid; 10 } 11 public void setRoleid(Integer roleid) { 12 this.roleid = roleid; 13 } 14 public String getRolename() { 15 return rolename; 16 } 17 public void setRolename(String rolename) { 18 this.rolename = rolename; 19 } 20 public List<Menu> getMenulist() { 21 return menulist; 22 } 23 public void setMenulist(List<Menu> menulist) { 24 this.menulist = menulist; 25 } 26 27 @Override 28 public String toString() { 29 return "RoleInfo [roleid=" + roleid + ", rolename=" + rolename + ", menulist=" + menulist + "]"; 30 } 31 32 }
第一種方法:利用嵌套語句查詢
1 <resultMap type="com.test.mybatis.model.RoleInfo" id="roleModel"> 2 <id column="id" property="roleid"/> 3 <result column="name" property="rolename"/> 4 <collection property="menulist" select="getMenu" column="id"> 5 6 </collection> 7 </resultMap> 8 9 <select id="getRoleInfo" resultMap="roleModel"> 10 select id,name from role 11 </select> 12 13 <select id="getMenu" resultType="com.test.mybatis.model.Menu"> 14 select m.id,m.name 15 from menu m join roleandmenu ram on m.id=ram.menuId 16 where ram.roleId=#{id} 17 </select>
1 @Test 2 public void testRoleAndMenu() throws IOException { 4 Reader reader = Resources.getResourceAsReader("mybatis.xml"); 5 SqlSessionFactory sqlsessionfac = new SqlSessionFactoryBuilder().build(reader); 6 SqlSession sqlsession = sqlsessionfac.openSession(); 7 try { 8 RoleAndMenuMapper mapper = sqlsession.getMapper(RoleAndMenuMapper.class); 9 System.out.println(JSONObject.toJSON(mapper.getRoleInfo())); 10 } catch (Exception e) { 11 // TODO: handle exception 12 e.printStackTrace(); 13 } finally { 14 sqlsession.close(); 15 } 17 }
結果:
原理如下:
1.mybatis先執行getRoleInfo這個查詢,獲取結果集
2.從ResultSet中逐一取出記錄,構建RoleInfo對象並為映射屬性賦值
3.賦值過程中發現目標menulist屬性配置了一個關聯集合(collection),此時執行id為collection標簽中select屬性值(getMenu)的查詢,並將當前記錄中的id屬性作為此查詢的參數。(association標簽同理)
4.將關聯查詢返回的結果映射到meunlist屬性
5.執行步驟2,直至ResultSet.next=false
6.返回查詢結果
這種方式的好處在於簡單易懂,通過簡單的配置就可以達到目標效果。不足之處在於如果結果集記錄條數過大,會造成較大的數據庫訪問消耗,因為在從ResultSet中取出記錄的時候每取一條,便執行一次關聯查詢,假設一次查詢的結果集有10條記錄,則數據庫的訪問次數為:關聯查詢次數(10)+返回結果集的查詢(1)=11次。
需要注意的地方
1.collection/association標簽的column屬性:當向關聯查詢傳遞的參數個數為1時,column的值應為結果集中的列名,而不是映射屬性名(property),上面的例子中,向關聯查詢傳遞id值,column的值應為id而不是roleid。
可以向關聯查詢傳遞多個參數,此時column的值為多個鍵值對,如下圖
此時向關聯查詢傳遞了兩個參數id和name,此時還應該將關聯的查詢的parameterType改為java.util.Map,否則關聯查詢無法接受參數
2.在進行單一類型樹形結構查詢的時候,需要注意關聯查詢的結果集中的列是否有作為查詢條件的列
這樣說可能比較別扭,以上面的menu表為例,有一個parent列用於存儲父部門的ID,使用嵌套查詢獲取以下實體類
1 import java.util.List; 2 3 public class MenuTree { 4 5 private Integer id; 6 private String name; 7 private List<Menu> children; 8 public Integer getId() { 9 return id; 10 } 11 public void setId(Integer id) { 12 this.id = id; 13 } 14 public String getName() { 15 return name; 16 } 17 public void setName(String name) { 18 this.name = name; 19 } 20 public List<Menu> getChildren() { 21 return children; 22 } 23 public void setChildren(List<Menu> children) { 24 this.children = children; 25 } 26 27 28 }
此時會產生一個問題:每個頂級菜單(parent為空的菜單)的子菜單只有一個查詢結果,這是因為在關聯查詢getSubMenu中沒有將id查詢出來,而關聯查詢和主查詢的resultMap一樣,所以關聯查詢在映射結果集的時候就會再次去執行關聯查詢,而由於本次關聯查詢並沒有取出id這個作為參數的屬性,所以實際上只執行了N(結果集記錄數)次關聯查詢。
因此,在這種情況中,必須在關聯查詢中查詢出id這個列,否則會查詢不出預期結果。
第二種方法:使用嵌套結果集
1 <resultMap type="com.test.mybatis.model.RoleInfo" id="roleModel"> 2 <id column="id" property="roleid"/> 3 <result column="name" property="rolename"/> 4 <collection property="menulist" ofType="com.test.mybatis.model.Menu"> 5 <id column="menuid" property="id"/> 6 <result column="menuname" property="name"/> 7 <result column="description" property="description"/> 8 <result column="parent" property="parent"/> 9 <result column="createdate" property="createdate"/> 10 <result column="modifydate" property="modifydate"/> 11 </collection> 12 </resultMap> 13 14 <select id="getRoleInfo" resultMap="roleModel"> 15 select 16 ram.roleid as id, 17 ro.name as name, 18 me.id as menuid, 19 me.name as menuname, 20 me.description, 21 me.parent, 22 me.createdate, 23 me.modifydate 24 from roleandmenu ram 25 left outer join role ro on ram.roleid=ro.id 26 left outer join menu me on ram.menuid=me.id 27 </select>
原理是通過關聯查詢,一次性將數據查詢出來,然后根據resultMap的配置進行轉換,構建目標實體類。
顯然,這種方法更為直接,只需要訪問一次數據庫就可以了,不會造成嚴重的數據庫訪問消耗。
但是以上是查詢出全部數據的情況,因為只有查詢出全部數據,才能得到最終結果。如果直接分頁的話,會導致數據被截斷,也就是collection中的數據殘缺。
這種情況最好的處理方式就是手動分頁,對主表分頁,對其他連接的表不分頁。
將上面的SQL改為
SELECT base.id, ro. NAME AS NAME, me.id AS menuid, me. NAME AS menuname, me.description, me.parent, me.createdate, me.modifydate FROM ( SELECT roleid AS id, menuid FROM roleandmenu LIMIT $ { pageNum }, $ { pageSize } ) AS base LEFT OUTER JOIN role ro ON base.id = ro.id LEFT OUTER JOIN menu me ON base.menuid = me.id
這里手動傳入pageNum,pageSize,對主表roleandmenu分頁,從表role和menu不分頁。
這樣就可以得到正確的數據。
此外,還有一種情況,如果把嵌套結果集的返回值類型全部改成HashMap的話,會導致menulist里只有一行數據
解決的辦法是,給collection標簽的javaType賦值為目標集合類型
找了很久,終於在官方文檔里找到了解釋
參考資料:http://www.jianshu.com/p/9e397d5c85fd