概述
MyBatis-20MyBatis高级结果映射【一对一映射(4种方式)】中我们介绍了4种方式实现一对一映射,本篇博文,一对多映射只有两种配置方式,都是使用collection标签进行的。
collection集合的嵌套结果映射
和association类似,集合的嵌套结果映射就是通过一次SQL查询将所有的结果查询出来,然后通过配置的结果映射,将数据映射到不同的对象中取。 在一对多的关系中,主表的一条数据会对应关联表中的多条数据,因此一般查询时会查询出多个结果,按照一对多的数据结果存储数据的时候,最终的结果会小于等于查询的总记录数。
在RBAC权限系统中,一个用户用于多个角色(在使用association是设定的特例,现在一个用户只能有一个角色),每个角色又是多个权限的集合,所以要渐进式的去实现一个SQL,查询出所有用户和用户拥有的角色,以及角色所包含的所有权限信息的两层嵌套结果。
SysUse实体类改造
为了能够存储一对多的数据,先对SysUser类进行修改
增加
public class SysUser{ // 原有属性, setter getter保持不变 /** * 用户角色: 一个用户拥有多个角色 , 一对多 */ private List<SysRole> roleList; public List<SysRole> getRoleList() { return roleList; } public void setRoleList(List<SysRole> roleList) { this.roleList = roleList; } }
UserMapper接口增加接口方法
/** * * * @Title: selectAllUserAndRoles * * @Description:获取所有用户及对应的角色 * * @return * * @return: List<SysUser> */ List<SysUser> selectAllUserAndRoles()
UserMapper.xml
<!-- 简化的配置 --> <resultMap id="userRoleListMap" extends="userMap" type="com.artisan.mybatis.xml.domain.SysUser" > <!-- sysRole相关的属性 property 对应实体类List<SysRole>属性名--> <collection property="roleList" columnPrefix="sysRole_" resultMap="roleMap"> </collection> </resultMap> <select id="selectAllUserAndRoles" resultMap="userRoleListMap"> SELECT u.id, u.user_name , u.user_password , u.user_email , u.user_info , u.create_time , u.head_img , r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id </select>
和一对一映射相比,一对多的userRoleListMap,就是把association改成collection, 然后将property设置为roleList,其他的属性保持不变。
collection用于配置一对多的关系,对应的属性必须是对象中的集合类型,因此这里是roleList。 另外resultMap只是为了配置数据库字段和实体属性的映射关系,因此其他都一样。 同时能存储一对多的数据结构肯定也能存储一对一的关系,所以一对一是一对多的一种特例。 collection支持的属性以及属性的作用和association完全相同。
为了简化配置,我们通过继承userMap来使用sys_user的映射关系,同时我们在UserMapper.xml中配置了roleMap的映射关系(更加合适的问题应该在RoleMapper.xml中,如果在RoleMapper.xml中,引用的时候一定要加上命名空间),因此直接饮用roleMap ,经过这两个方式的简化,最终的userRoleListMap如上
总结下:一对多配置变化的地方是 association变为collection, property由role变为了roleList
单元测试
@Test public void selectAllUserAndRolesTest() { logger.info("selectAllUserAndRolesTest"); // 获取SqlSession SqlSession sqlSession = getSqlSession(); try { // 获取UserMapper接口 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 调用selectAll,查询全部用户 List<SysUser> userList = userMapper.selectAllUserAndRoles(); // 结果不为空 Assert.assertNotNull(userList); // 结果大于0 Assert.assertTrue(userList.size() > 0); logger.info("userList总数为:" + userList.size()); for (SysUser sysUser : userList) { logger.info("用户名:" + sysUser.getUserName()); for (SysRole sysRole : sysUser.getRoleList()) { logger.info("\t角色名:" + sysRole.getRoleName()); } } } catch (Exception e) { e.printStackTrace(); } finally { sqlSession.close(); logger.info("sqlSession close successfully "); } }
我们在 Assert.assertNotNull(userList); 加上个断点,debug看下数据
点进去一个看下
从上图可以看到一个用于已经拥有两个角色,实现了一对多的查询。
接下来看下日志
2018-05-02 02:02:26,338 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully 2018-05-02 02:02:26,343 INFO [main] (BaseMapperTest.java:29) - reader close successfully 2018-05-02 02:02:26,346 INFO [main] (UserMapperTest.java:1133) - selectAllUserAndRolesTest 2018-05-02 02:02:26,415 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: SELECT u.id, u.user_name , u.user_password , u.user_email , u.user_info , u.create_time , u.head_img , r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id 2018-05-02 02:02:26,538 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 2018-05-02 02:02:26,586 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, create_time, head_img, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time 2018-05-02 02:02:26,587 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 02:02:26,598 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 02:02:26,600 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, test@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 02:02:26,602 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 3 2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1146) - userList总数为:2 2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1148) - 用户名:admin 2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1150) - 角色名:管理员 2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户 2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1148) - 用户名:artisan 2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户 2018-05-02 02:02:26,608 INFO [main] (UserMapperTest.java:1157) - sqlSession close successfully
MyBatis的处理规则
通过日志可以清楚地看到,SQL执行的结果数有3条,用户输出的数量确实2条,也就是说本来查询出的3条结果经过MyBatis对collection数据的处理后,变成了2条。
从日志中,我们知道第一个用户拥有两个角色,所以转换为一对多的数据结构后就变成了两套结果,那么 MyBatis又是怎么知道要处理成这样的结果呢?
先来看MyBatis是如何要知道合并admin的两条数据的,为什么不把test这条数据也合并进去呢?
MyBatis在处理结果的时候,会判断结果是否相同,如果是相同的结果,则只会保留第一个结果。 所以这个问题的关键点就是MyBatis是如何判断结果是否相同。 最简单的情况就是在映射配置中至少有一个id标签
<id property="id" column="id" />
我们对id的理解一般是,它配置的字段为表的主键(联合主键时可以配置多个id标签),因为MyBatis的resultMap只用于配置结果如何映射,并不知道这个表具体如何。 id的唯一作用就是在嵌套的映射配置中判断数据是否相同。 .当配置id标签时,MyBatis只需要逐条比较所有数据中id标签的字段值是否相同即可。 在配置嵌套结果查询时,配置id标签提高处理效率。
这样一来,上面的查询就不难理解了,因为前两套数据的userMap部分的id相同,所以他们属于同一个用户,因子这条数据会合并到同一个用户中。
为了更加清楚的理解id的作用,我们队userMap的映射进行如下修改。
<resultMap id="userMap" type="com.artisan.mybatis.xml.domain.SysUser"> <id property="userPassword" column="userPassword" /> <result property="userName" column="user_name" /> <result property="userPassword" column="user_password" /> <result property="userEmail" column="user_email" /> <result property="userInfo" column="user_info" /> <result property="headImg" column="head_img" jdbcType="BLOB" /> <result property="createTime" column="create_time" jdbcType="TIMESTAMP" /> </resultMap>
在测试数据中,用户的密码均为 123456
如果把密码最为id,按照上面的逻辑,3条数据就会合并为1条数据,修改后,再次执行单元测试。
2018-05-02 12:24:27,161 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully 2018-05-02 12:24:27,161 INFO [main] (BaseMapperTest.java:29) - reader close successfully 2018-05-02 12:24:27,173 INFO [main] (UserMapperTest.java:1133) - selectAllUserAndRolesTest 2018-05-02 12:24:27,253 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: SELECT u.id, u.user_name , u.user_password , u.user_email , u.user_info , u.create_time , u.head_img , r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id 2018-05-02 12:24:27,383 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 2018-05-02 12:24:27,433 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, create_time, head_img, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time 2018-05-02 12:24:27,433 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 12:24:27,443 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 12:24:27,443 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, test@artisan.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0 2018-05-02 12:24:27,443 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 3 2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1146) - userList总数为:1 2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1148) - 用户名:admin 2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1150) - 角色名:管理员 2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户 2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1157) - sqlSession close successfully
userList总数为:1 用户名:admin 角色名:管理员 角色名:普通用户
用户信心保留的是第一条数据的信心,因此用户名是admin . 角色为什么不是3个呢? 因为“普通用户”这个角色重复了,所以也只保留第一个出现的“普通用户”。 因为MyBatis会对嵌套查询的每一级对象都进行属性比较。 MyBatis会首先比较顶层的对象,如果SysUser相同,就继续比较SysRole部分,如果SysRole不同,就会增加一个sysRole,两个SysROle相同就保留前一个。 假设SysRole还有下一级,仍然按照该规则去比较。
通过上述这个例子应该明白了id的作用了,需要注意的是,很肯能出现一种没有配置id的情况。 当没有配置id的时候,MyBatis就会把resultMap中配置的说哟字段进行比较,如果所有字段的值都相同就合并,只要有一个字段值不同,就不合并。
在嵌套结果配置id属性时,如果查询中没有查询id属性配置的列,就会导致id对应的值为null.这种情况下,所有的id都相同,因此会使嵌套的集合中只有一条数据。 所以在配置id列时,查询语句中必须包含该列。
可以对userMap再次改造,将id标签改为result标签,执行结果是一样的,由于MyBatis要对所有字段字段进行比较,因此当字段数为M时,如果查询结果有N条,就需要进行M*N,相比配置id时的N次比较,效率差很多。 所以尽量配置id标签.
<result property="id" column="id"/>
两层嵌套
在RBAC权限系统中,除了一个用户对应多个角色外,每个角色还会对应多个权限,在上个例子的基础上我们增加一级,获取角色对应的所有权限。
PrivilegeMap.xml增加映射
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <!-- 当Mapper接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 --> <mapper namespace="com.artisan.mybatis.xml.mapper.PrivilegeMapper"> <resultMap id="privilegeMap" type="com.artisan.mybatis.xml.domain.SysPrivilege"> <id property="id" column="id" /> <result property="privilegeName" column="privilege_name" /> <result property="privilegeUrl" column="privilege_url" /> </resultMap> </mapper>
SysRole实体类改造
增加
/** * 一对多,权限集合 */ List<SysPrivilege> privilegeList; public List<SysPrivilege> getPrivilegeList() { return privilegeList; } public void setPrivilegeList(List<SysPrivilege> privilegeList) { this.privilegeList = privilegeList; }
RoleMapper.xml文件中增加如下resultMap
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <!-- 当Mapper接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 --> <mapper namespace="com.artisan.mybatis.xml.mapper.RoleMapper"> <resultMap id="rolePrivilegeListMap" type="com.artisan.mybatis.xml.domain.SysRole" extends="com.artisan.mybatis.xml.mapper.UserMapper.roleMap"> <collection property="privilegeList" columnPrefix="privilege_" resultMap="com.artisan.mybatis.xml.mapper.PrivilegeMapper.privilegeMap" /> </resultMap> </mapper>
我们创建了角色权限映射,继承了roleMap,嵌套了privilegeList属性,直接使用PrivilegeMapper.xml中的privilegeMap。
UserMapper.xml改造
<resultMap id="userRoleAndPrivilegeListMap" extends="userMap" type="com.artisan.mybatis.xml.domain.SysUser" > <collection property="roleList" columnPrefix="sysRole_" resultMap="com.artisan.mybatis.xml.mapper.RoleMapper.rolePrivilegeListMap"> </collection> </resultMap>
到这里我们就配置好了一个两层嵌套的映射,为了得到权限信息,还需要修改SQL进行关联
<select id="selectAllUserAndRolesAndPrivileges" resultMap="userRoleAndPrivilegeListMap"> select u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time, r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time, p.id sysRole_privilege_id, p.privilege_name sysRole_privilege_privilege_name, p.privilege_url sysRole_privilege_privilege_url from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id inner join sys_role_privilege rp on rp.role_id = r.id inner join sys_privilege p on p.id = rp.privilege_id </select>
这里需要特别注意sys_privilege表中的别名。因为sys_privilege嵌套在rolePrivilegeListMap中,前缀名是 privilege_
而rolePrivilegeListMap的前缀是sysRole_
所以rolePrivilegeListMap中的privilegeMap的前缀就变测过了 sysRole_privilege_
在嵌套中,这个前缀需要叠加,一定不要写错,所以SQL如下
单元测试
@Test public void selectAllUserAndRolesAndPrivilegesTest() { logger.info("selectAllUserAndRolesAndPrivilegesTest"); // 获取SqlSession SqlSession sqlSession = getSqlSession(); try { // 获取UserMapper接口 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 调用selectAll,查询全部用户 List<SysUser> userList = userMapper.selectAllUserAndRolesAndPrivileges(); // 结果不为空 Assert.assertNotNull(userList); // 结果大于0 Assert.assertTrue(userList.size() > 0); logger.info("userList总数为:" + userList.size()); for (SysUser sysUser : userList) { logger.info("用户名:" + sysUser.getUserName()); for (SysRole sysRole : sysUser.getRoleList()) { logger.info("\t角色名:" + sysRole.getRoleName()); for (SysPrivilege sysPrivilege : sysRole.getPrivilegeList()) { logger.info("\t\t权限名:" + sysPrivilege.getPrivilegeName()); } } } } catch (Exception e) { e.printStackTrace(); } finally { sqlSession.close(); logger.info("sqlSession close successfully "); } }
Assert.assertNotNull(userList); 加个断点,debug看下数据
日志
2018-05-02 20:10:08,202 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully 2018-05-02 20:10:08,207 INFO [main] (BaseMapperTest.java:29) - reader close successfully 2018-05-02 20:10:08,211 INFO [main] (UserMapperTest.java:1164) - selectAllUserAndRolesAndPrivilegesTest 2018-05-02 20:10:08,287 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: select u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time, r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time, p.id sysRole_privilege_id, p.privilege_name sysRole_privilege_privilege_name, p.privilege_url sysRole_privilege_privilege_url from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id inner join sys_role_privilege rp on rp.role_id = r.id inner join sys_privilege p on p.id = rp.privilege_id 2018-05-02 20:10:08,411 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 2018-05-02 20:10:08,448 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time, sysRole_privilege_id, sysRole_privilege_privilege_name, sysRole_privilege_privilege_url 2018-05-02 20:10:08,449 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 1, 用户管理, /users 2018-05-02 20:10:08,466 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 3, 系统日志, /logs 2018-05-02 20:10:08,468 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 2, 角色管理, /roles 2018-05-02 20:10:08,469 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 4, 人员维护, /persons 2018-05-02 20:10:08,473 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, admin@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 5, 单位维护, /companies 2018-05-02 20:10:08,475 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, test@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 4, 人员维护, /persons 2018-05-02 20:10:08,477 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, test@artisan.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 5, 单位维护, /companies 2018-05-02 20:10:08,478 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 7 2018-05-02 20:10:08,479 INFO [main] (UserMapperTest.java:1177) - userList总数为:2 2018-05-02 20:10:08,479 INFO [main] (UserMapperTest.java:1179) - 用户名:admin 2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1181) - 角色名:管理员 2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:用户管理 2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:系统日志 2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:角色管理 2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1181) - 角色名:普通用户 2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1183) - 权限名:人员维护 2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1183) - 权限名:单位维护 2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1179) - 用户名:artisan 2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1181) - 角色名:普通用户 2018-05-02 20:10:08,484 INFO [main] (UserMapperTest.java:1183) - 权限名:人员维护 2018-05-02 20:10:08,484 INFO [main] (UserMapperTest.java:1183) - 权限名:单位维护 2018-05-02 20:10:08,485 INFO [main] (UserMapperTest.java:1191) - sqlSession close successfully
collection集合的嵌套查询
同association关联的嵌套查询这种方式类似,collection也会执行额外的SQL查询。 后续单开篇介绍。