最近开发中遇到了很多树形结构数据的需要,利用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