MyBatis多表操作
前言
在前面的两个小节里,我们已经初步接触到MyBatis,并且通过MyBatis实现了单表的增删改查操作,但在实际开发过程中,经常遇到的是多表之间的操作,MyBatis在多表操作方面也提供非常方便的工具用于将结果集映射到对象中,这一节,我们将详细学习这一部分。
多表操作
由于本节涉及到多表操作,在前面建立的数据表明显不符合,所以这里我们需要再建立一些表以及插入一些数据
本节所使用的表以及数据均来自刘增辉老师的《MyBatis从入门到精通》
create table sys_user (
id bigint not null auto_increment comment '用户ID',
user_name varchar(50) comment '用户名',
user_password varchar(50) comment '密码',
user_email varchar(50) comment '邮箱',
create_time datetime comment '创建时间',
primary key (id)
);
alter table sys_user comment '用户表';
create table sys_role (
id bigint not null auto_increment comment '角色ID',
role_name varchar(50) comment '角色名',
enabled int comment '有效标志',
create_by bigint comment '创建人',
create_time datetime comment '创建时间',
primary key (id)
);
alter table sys_role comment '角色表';
create table sys_privilege (
id bigint not null auto_increment comment '权限ID',
privilege_name varchar(50) comment '权限名称',
privilege_url varchar(200) comment '权限URL',
primary key (id)
);
alter table sys_privilege comment '权限表';
create table sys_user_role (
user_id bigint comment '用户ID',
role_id bigint comment '角色ID'
);
alter table sys_user_role comment '用户角色关联表';
create table sys_role_privilege (
role_id bigint comment '角色ID',
privilege_id bigint comment '权限ID'
);
alter table sys_role_privilege comment '角色权限关联表';
测试数据
insert into `sys_user`
values
(1, 'admin', '123456', 'admin@mybatis', '管理员', null, now()),
(1001, 'test', '123456', 'test@mybatis', '测试用户', null, now());
insert into sys_role
values
(1, '管理员', '1', '1', now()),
(2, '普通用户', '1', '1', now());
insert into sys_user_role values (1, 1), (1, 2), (1001, 2);
insert sys_privilege
values
(1, '用户管理', '/users'),
(2, '角色管理', '/roles'),
(3, '系统日志', '/logs'),
(4, '人员维护', '/persons'),
(5, '单位维护', '/companies');
insert sys_role_privilege
values (1, 1), (1, 3), (1, 2), (2, 4), (2, 5);
对应的实体类根据数据库的字段建立就好了。
关于每个表的单表操作,在前面一个小节已经研究过了,所以在这个小节里,就不演示单表的操作了。
多表操作,本质上其实就是连接多个表,然后查询出数据,根据关联对象之间的关系,又可以分为1对1操作,1对多操作,多对多操作(本质上而言其实也是1对多),所以接下来,我们分两个部分来看如何通过MyBatis来操作
1对1操作
假设我们要根据用户的ID查询出用户的角色,并且假定一个用户只有一个角色(当然,实际上不止),这里以1001号用户为例,其在数据库中也仅有一个角色,所以符合我们操作的要求。
为了能通过MyBatis自动封装,我们在SysUser
中增加一个字段SysRole
public class SysUser {
// 其他字段与数据库保持一致即可
private SysRole role;
// set() get() toString()
}
在查询操作中,我们可以通过下面的方式来获取数据
<select id="selectUserAndRoleById" resultType="domain.SysUser">
select
u.id,
u.user_name userName,
u.user_password userPassword,
u.user_email userEmail,
u.create_time createTime,
<!--
注意从这里开始的别名是"role.XXX",因为字段中是role
为了能够自动注入,所以需要采用obj.attr的形式,
如果有多级对象,则是 a.b.c这种形式
-->
r.id "role.id",
r.role_name "role.roleName",
r.enabled "role.enable",
r.create_by "role.createBy",
r.create_time "role.createTime"
from sys_user u
join sys_user_role ur on u.id = ur.user_id
join sys_role r on r.id = ur.role_id
where u.id = #{id}
</select>
上面的实现方式从结果来看是没有问题的,但是从工程的角度来讲,其实不太好,尤其是当存在多个不同类型的查询,比如根据ID,根据名称,根据邮箱地址等,我们需要编写多份的代码,并且其中的select部分基本上是不变的,也就是带来非常明显的冗余了。
更好地解决方案是使用MyBatis中的resultMap
,通过resultMap
来封装,可以实现代码复用的目的
<resultMap id="userRoleMap" type="domain.SysUser">
<id property="id" column="id"/>
<!--其他的字段-->
<result property="role.id" column="r.id"/>
<!--其他的字段-->
</resultMap>
<select id="selectUserAndRoleById" resultMap="userRoleMap">
...
这里根据对应的字段调整一下,只需要能正确映射就行
</select>
不过上面的内容语义不明显,更好的方式是使用resutlMap
的<association>
标签来关联对象,如下
<resultMap id="userRoleMap" type="domain.SysUser">
<id property="id" column="id"/>
<!--其他的字段-->
<!--
注意这里,使用的是association,association的使用跟resultMap是类似的
并且使用多了columnPrefix属性,为了区分来自不同表的字段,
如果是多级的嵌套,则需要指定多级,如 role_pri_XXX,一个层次的columnPrefix会
去过滤每一次匹配的前缀
当然,在查询的时候也需要将对应的前缀标注出来
-->
<association property="role" javaType="domain.SysRole" columnPrefix="role_">
<result property="id" column="id"/>
<!--其他的字段-->
</association>
</resultMap>
<!--注意下面的内容 role_也即是columnPrefix=""中指定的字段-->
<select>
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
</select>
通过上面的方式,当需要的时候,就可以直接指定查询的resultMap="userRoleMap"
即可,已经减少了一部分的重复操作了,但是,上面的方式仍然不是合适的,因为既然有user对应的map,那实际上将role对应的字段也封装到map中,然后直接调用即可,这样,多个使用到role的地方都可以直接使用了
首先在SysRoleMapper.xml定义对应的roleMap,当然,放在其他的mapper里也是可以,但是放在SysRoleMapper.xml是最合适的
<mapper namespace="mapper.SysRoleMapper">
<resultMap id="roleMapper" type="domain.SysRole">
<id property="id" column="id"/>
<!--其他的字段-->
</resultMap>
</mapper>
整理完之后的userRoleMap
内容如下
<resultMap id="userRoleMap" type="domain.SysUser">
<id property="id" column="id"/>
<!--其他的字段-->
<!--这里使用resultMap来指定其他的resultMap,如果不在本文件,则使用全限定名-->
<association property="role" columnPrefix="role_" resultMap="mapper.SysRoleMapper.roleMapper"/>
</resultMap>
经过上面的整理之后,现在的整体结构就变得非常灵活了,特别是当我们需要组合多个对象的时候,通过这种方式,可以实现只需要定义一个resultMap,然后在多处使用
1对多操作
有了上面封装1对1的操作过程作为基础,实现一对多就容易很多了,只需要将<association>
替换为<collection>
即可,当然,由于上面为了方便,直接在SysUser中定义了一个SysRole对象,但实际上我们知道,一个用户是可以对应多个角色的,所以,在SysUser中应该定义的是一个SysRole容器,比如list或者set等,也就是实际上1对多的操作啦
<resultMap id="userRoleMap" type="domain.SysUser">
<!--注意这里-->
<collection property="role" columnPrefix="role_" resultMap="mapper.SysRoleMapper.roleMapper"/>
</resultMap>
可以看到,因为为role对象定义roleMap,所以,当改动userRole时,其他的内容完全不需要改动
一个完整的例子
在上面的两步操作中,我们已经充分体验到了MyBatis中的resultMap
、assocation
以及collection
提供的便利,下面我们通过完整的例子,来加深对其认识
这里通过用户ID,获取其所有的角色以及所有角色对应的权限
将对应的实体类调整为如下
SysUser
public class SysUser {
// 一个用户可能对应多个角色
private List<SysRole> role;
}
SysRole
public class SysRole {
// 一个角色可能有多个权限
private List<SysPrivilege> privilegeList;
}
然后为每个实体类编写对应的resultMap
,这个参考上面的编写方式就行啦,这里就不贴代码了
接下来组合多个resultMap,这里我们采用自底向上的方式
<resultMap id="rolePrivilegeMap" type="domain.SysRole">
<id property="id" column="id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="roleName" column="role_name"/>
<result property="enabled" column="enabled"/>
<!--组装privilegeMap-->
<collection property="privilegeList" columnPrefix="pri_" resultMap="mapper.SysUserMapper.privilegeMap"/>
</resultMap>
<resultMap id="userRoleMap" type="domain.SysUser">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="userPassword" column="user_password"/>
<result property="userEmail" column="user_email"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<!--组装rolePrivilegeMap-->
<collection property="role" columnPrefix="role_" resultMap="mapper.SysRoleMapper.rolePrivilegeMap"/>
</resultMap>
通过上面的两层组装之后,当我们需要使用的时候,就可以直接指定resultMap="userRoleMap"
即可啦
关于ResultMap,还有一个小点需要注意,如果查询的数据中不包含某些字段,而resultMap中有该字段时,MyBatis会忽略该字段,所以,一个resultMap可以复用在其他场景,即使查询的字段跟resultMap中的字段不完全匹配,只要resultMap中包含我们需要的字段即可
discriminator
在ResultMap中,还有一个<discriminator>
,该标签的用途在于,根据不同的字段值进行分类,比如在上面的案例中,有一些角色是启用的,有一些是不允许启用的,那么,对于不允许启用的角色,我们就不需要获取其角色以及权限信息,所以,这时,可以通过discriminator来实现根据不同的值来映射到不同的resutMap中,如下面所示
<resultMap id="userMap" type="domain.SysUser">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="userPassword" column="user_password"/>
<result property="userEmail" column="user_email"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<resultMap id="userRoleMap" type="domain.SysUser">
<!--根据role_enabled的状态来选择不同的查询-->
<discriminator javaType="int" column="role_enabled">
<case value="1" resultMap="userRoleMapSelect" />
<!--只获取用户的基本信息,不获取角色以及权限信息-->
<case value="0" resultMap="userMap"/>
</discriminator>
</resultMap>
<!--直接继承userMap,可以避免编写过多的result标签-->
<resultMap id="userRoleMapSelect" type="domain.SysUser" extends="userMap">
<collection property="role" columnPrefix="role_" resultMap="mapper.SysRoleMapper.rolePrivilegeMap"/>
</resultMap>
通过上面的例子,可以看到discriminator
的强大之处了,在使用discriminator
的时候需要注意,discriminator
是作用在当前的resultMap的,也就是说,discriminator
中的resultMap
封装的是当前的result
中的内容,而不是决定子查询中的内容
总结
本小节主要学习了MyBatis中的多表查询,通过MyBatis中的resultMap
以及resultMap
中的association
、collection
,可以实现一对一,一对多查询中结果的自动封装,而通过discriminator
则可以根据不同的数值来选择返回不同的resultMap
,通过resultMap
中的extends
属性,可以复用一个已经存在的resultMap
,通过多个resultMap
的复用,可以极大地提高代码的复用率,使得代码更加简洁。