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 中查询出的角色 idselectRoleByUserId 是一个只有一层嵌套的一对多映射配置,通过调用 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

posted @ 2020-01-12 17:07  Marlon康  阅读(260)  评论(0编辑  收藏  举报