MyBatis从入门到精通(第6章):MyBatis 高级查询->6.1.2高级结果映射之一对多映射
jdk1.8、MyBatis3.4.6、MySQL数据库5.6.45、IntelliJ IDEA 2019.3.1
本章主要包含的内容为 MyBatis 的高级结果映射,主要处理数据库一对一、一对多的查询,另外就是在 MyBatis 中使用存储过程的方法,处理存储过程的入参和出参方法,最后会介绍 Java 中的枚举方法和数据库表字段的处理方法。
6.1 高级结果映射
在关系型数据库中,我们经常要处理一对一、一对多的关系。
在 RBAC 权限系统中还存在着一个用户拥有多个角色、一个角色拥有多个权限这样复杂的嵌套关系。使用已经学会的 MyBatis 技巧都可以轻松地解决这种复杂的关系。在面对这种关系的时候,我们可能要写多个方法分别查询这些数据,然后再组合到一起。这种处理方式特别适合用在大型系统上,由于分库分表,这种用法可以减少表之间的关联查询,方便系统进行扩展。
但是在一般的企业级应用中,使用 MyBatis 的高级结果映射便可以轻松地处理这种一对一、一对多的关系。
一对多映射只有两种配置方式,都是使用 collection 标签进行的,下面来看具体的介绍。
在 RBAC 权限系统中,一个用户拥有多个角色(注意,使用association 是设定的特例,限制一个用户只有一个角色),每个角色又是多个权限的集合,所以要渐进式地去实现一个 SQL,查询出所有用户和用户拥有的角色,以及角色所包含的所有权限信息的两层嵌套结果。
先来看如何实现一层嵌套的结果,为了能够存储一对多的数据,先对 SysUser 类进行修改,代码如下。
在 SysUser 类中增加 roleList 属性用于存储用户对应的多个角色。
在 UserMapper.xml 中创建 resultMap ,代码如下。
和 6.1.1.3 中的方式对比会很容易发现,此处就是把 association 改成了 collection ,然后将 property 设置为了 roleList,其他的 id 和 result 的配置都还一样。仔细想想应该不难理解,collection 用于配置一对多关系,对应的属性必须是对象中的集合类型,因此这里是 roleList。另外,resultMap 只是为了配置数据库字段和实体属性的映射关系,因此其他都一样。同时能存储一对多的数据结构肯定也能存储一对一关系,所以一对一像是一对多的一种特例。collection 支持的属性以及属性的作用和association 完全相同。
上一节中,我们逐步对 resultMap 进行了简化,在这一节,因为有了上一节的基础,因此可以大刀阔斧地对这个 resultMap 进行快速简化。首先,SysUser 中的属性可以直接通过继承 userMap 来使用 sys_user 的映射关系,其次在 RoleMapper.xml 中的 roleMap 映射包含了 sys_role 的映射关系,因此可以直接引用 roleMap,经过这两个方式的简化,最终的userRoleListMap 如下。
仿照上一节的 selectUserAndRoleById 2 方法,创建selectAllUserAndRoles 方法,代码如下。
这个方法用于查询所有用户及其对应的角色,sys_role 对应的查询列都增加了以“role_”作为前缀的别名。
在 UserMapper 接口中增加如下的对应方法。
/** * 获取所有的用户以及对应的所有角色 * * @return */ List<SysUser> selectAllUserAndRoles();
针对该方法,在 UserMapperTest 中添加如下测试。
从上图已经可以看到,第一个用户拥有两个角色,实现了一对多的查询。再来看一下测试代码输出的日志。
通过日志可以清楚地看到,SQL 执行的结果数有 3 条,后面输出的用户数是 2,也就是说本来查询出的 3 条结果经过 MyBatis 对 collection 数据的处理后,变成了两条。
提示!
在嵌套结果配置 id 属性时,如果查询语句中没有查询 id 属性配置的列,就会导致 id 对应的值为 null。
这种情况下,所有值的 id 都相同,因此会使嵌套的集合中只有一条数据。所以在配置 id 列时,查询语句中必须包含该列。
在 RBAC 权限系统中,除了一个用户对应多个角色外,每一个角色还会对应多个权限。所以在现有例子的基础上可以再增加一级,获取角色对应的所有权限。
如果在 PrivilegeMapper.xml 中没有 privilegeMap 映射配置,就在该配置文件中添加如下代码。
<resultMap id="privilegeMap" type="tk.mybatis.simple.model.SysPrivilege"> <id property="id" column="id"/> <result property="privilegeName" column="privilege_name"/> <result property="privilegeUrl" column="privilege_url"/> </resultMap>
然后在 SysRole 类中添加如下属性和方法。
/** * 角色包含的权限列表 */ List<SysPrivilege> privilegeList; public List<SysPrivilege> getPrivilegeList() { return privilegeList; } public void setPrivilegeList(List<SysPrivilege> privilegeList) { this.privilegeList = privilegeList; }
在 RoleMapper.xml 文件中,增加如下 resultMap 配置。
<resultMap id="rolePrivilegeListMap" extends="roleMap" type="tk.mybatis.simple.model.SysRole"> <collection property="privilegeList" columnPrefix="privilege_" resultMap="tk.mybatis.simple.mapper.PrivilegeMapper.privilegeMap"/> </resultMap>
我们创建了角色权限映射,继承了 roleMap,嵌套了 privilegeList 属性,直接使用了 PrivilegeMapper.xml 中的 privilegeMap 。
最后还要修改 UserMapper.xml 中的 userRoleListMap ,代码如下。
完成以上步骤就配置好了一个两层嵌套的映射。为了得到权限信息,还需要修改 SQL 进行关联,代码如下。
<select id="selectAllUserAndRoles" resultMap="userRoleListMap"> select u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time, r.id role_id, r.role_name role_role_name, r.enabled role_enabled, r.create_by role_create_by, r.create_time role_create_time, p.id role_privilege_id, p.privilege_name role_privilege_privilege_name, p.privilege_url role_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 中,而 rolePrivilegeListMap 的前缀是“role_”,所以 rolePrivilegeListMap 中 privilegeMap 的前缀就变成了“role_privilege_”。在嵌套中,这个前缀需要叠加,一定不要写错。
为了加深印象,利用上面的 rolePrivilegeListMap 实现一个查询角色和对应权限的方法。在 RoleMapper.xml 中添加如下方法。
<select id="selectAllRoleAndPrivileges" resultMap="rolePrivilegeListMap"> select r.id, r.role_name, r.enabled, r.create_by, r.create_time, p.id privilege_id, p.privilege_name privilege_privilege_name, p.privilege_url privilege_privilege_url from sys_role r 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 对应列的别名,请自行在 RoleMapper 中添加对应的接口,并且在 RoleMapperTest 中添加该方法的测试。
此处通过使用 rolePrivilegeListMap ,大家可以了解这样一个映射配置:它不仅可以被嵌套的配置引用,其本身也可以使用。一个复杂的映射就是由这样一个基本的映射配置组成的。通常情况下,如果要配置一个相当复杂的映射,一定要从基础映射开始配置,每增加一些配置就进行对应的测试,在循序渐进的过程中更容易发现和解决问题。
虽然 association 和 collection 标签是分开介绍的,但是这两者可以组合使用或者互相嵌套使用,也可以使用符合自己需要的任何数据结构,不需要局限于数据库表之间的关联关系。
6.1.2.2 collection 集合的嵌套查询
仍然以关联的嵌套结果中的 selectAllUserAndRoles 为基础,以上一节最后的两层嵌套结果为目标,将该方法修改为集合的嵌套查询方式。
面以自下而上的过程来实现这样一个两层嵌套的功能,并且这个自下而上的过程中的每一个方法都是一个独立可用的方法,最后的结果都是以前一个方法为基础的。把所有对象设置为延迟加载,因此每个方法都可以单独作为一个普通(没有嵌套)的查询存在。
首先在 PrivilegeMapper.xml 中添加如下方法。
<select id="selectPrivilegeByRoleId" resultMap="privilegeMap"> select p.* from sys_privilege p inner join sys_role_privilege rp on rp.privilege_id = p.id where role_id = #{roleId} </select>
这个方法通过角色 id 获取该角色对应的所有权限信息,可以在PrivilegeMapper 接口中增加相应的方法。这是一个很常见的方法,许多时候都需要这样一个方法来获取角色包含的所有权限信息。
下一步,在 RoleMapper.xml 中配置映射和对应的查询方法,代码如下。
在上面代码中要注意 column 属性配置的{roleId=id},roleId 是 select 指定方法 selectPrivilegeByRoleId 查询中的参数,id 是当前查询selectRoleByUserId 中查询出的角色 id。selectRoleByUserId 是一个只有一层嵌套的一对多映射配置,通过调用 PrivilegeMapper 的selectPrivilegeByRoleId 方法,很轻易就实现了嵌套查询的功能。针对这个方法,大家也要添加相应的接口方法进行测试。
终于要轮到顶层的用户信息了,在 UserMapper.xml 中添加如下映射和查询,代码如下。
<resultMap id="userRoleListMapSelect" extends="userMap" type="tk.mybatis.simple.model.SysUser"> <collection property="roleList" fetchType="lazy" select="tk.mybatis.simple.mapper.RoleMapper.selectRoleByUserId" column="{userId=id}"/> </resultMap>
<select id="selectAllUserAndRolesSelect" resultMap="userRoleListMapSelect"> select u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time from sys_user u where u.id = #{id} </select>
这里也需要注意,collection 的属性 column 配置为{userId=id},将当前查询用户中的 id 赋值给 userId,使用 userId 作为参数再进行selectRoleByUserId 查询。因为所有嵌套查询都配置为延迟加载,因此不存在 N+1 的问题。
在 UserMapper 接口中添加如下方法。
/** * 通过嵌套查询获取指定用户的信息,以及用户的角色和权限信息 * * @param id * @return */ SysUser selectAllUserAndRolesSelect(Long id);
然后在 UserMapperTest 中添加相应的测试,代码如下。
@Test public void testSelectAllUserAndRolesSelect(){ //获取 sqlSession SqlSession sqlSession = getSqlSession(); try { //获取 UserMapper 接口 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); SysUser user = userMapper.selectAllUserAndRolesSelect(1L); System.out.println("用户名:" + user.getUserName()); for(SysRole role: user.getRoleList()){ System.out.println("角色名:" + role.getRoleName()); for(SysPrivilege privilege : role.getPrivilegeList()){ System.out.println("权限名:" + privilege.getPrivilegeName()); } } } finally { //不要忘记关闭 sqlSession sqlSession.close(); } }
由于这里是多层嵌套,并且是延迟加载,因此这段测试会输出很长的日志,日志如下。
简单分析这段日志,当执行 selectAllUserAndRolesSelect 方法后,可以得到 admin 用户的信息,由于延迟加载,此时还不知道该用户有几个角色。当调用 user.getRoleList ()方法进行遍历时,MyBatis 执行了第一层的嵌套查询,查询出了该用户的两个角色。对这两个角色进行遍历获取角色对应的权限信息,因为已经有两个角色,所以分别对两个角色进行遍历时会查询两次角色的权限信息。特别需要注意的是,之所以可以根据需要查询数据,除了和 fetchType 有关,还和全局的aggressiveLazyLoading 属性有关,这个属性在介绍 association 时被配置成了 false,所以才会起到按需加载的作用。
6.1.3 鉴别器映射
鉴别器是一种很少使用的方式,在使用前一定要完全掌握,没有把握的情况下要尽可能避免使用。
===========================================================
end