一对一映射其实就是我们常说的一对一查询。在生活中,到处充斥着一对一、一对多的例子。我们之前说过的那个用户-角色-权限的系统就是一个活生生的案例。当存在比较复杂的对应关系时,我们可能要写多个方法来分别查询这些数据,然后再组合到一起。这种处理方式特别适合用在大型系统上,由于分库分表,这些用法可以减少表与表之间的关联查询,方便系统进行扩展。但是在一般的企业级应用中,使用MyBatis的高级结果映射便可以轻松的处理这种一对一、一对多的关系。
- 使用自动映射处理一对一关系
我们假设在之前提到的那个系统模型中一个用户只能拥有一个角色。那么我们在之前说过的selectRoleByUserId()方法实际上就已经是一个一对一的关系了。一对一的关系因为不需要考虑是否存在重复数据,所以使用起来很简单,而且可以直接使用MyBatis的自动映射。接下来,我们参考之前说过的selectRoleByUserId(Long userId)方法,使用自动映射实现在查询用户信息的同时获取用户拥有的角色。之前我们使用这个方法是根据用户名查询用户的角色名,查询结果是先显示角色名,再显示用户名,现在我们需要先显示用户名,再显示角色名。所以我们得先改造一下之前的User实体类,增加一个Role字段。增加字段后的User实体类的属性有:
private Long id; //用户ID
private String userName; //用户名
private String userPassword; //密码
private String userEmail; //Email
private String userInfo; //简介
private byte[] headImg; //头像
private Date createTime; //创建时间
private Role role;
之后完整的实体类User的属性和方法就具有如下这些:
package pojo;
import java.util.Arrays;
import java.util.Date;
public class User {
private Long id; //用户ID
private String userName; //用户名
private String userPassword; //密码
private String userEmail; //Email
private String userInfo; //简介
private byte[] headImg; //头像
private Date createTime; //创建时间
private Role role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
public String getUserInfo() {
return userInfo;
}
public void setUserInfo(String userInfo) {
this.userInfo = userInfo;
}
public byte[] getHeadImg() {
return headImg;
}
public void setHeadImg(byte[] headImg) {
this.headImg = headImg;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", userPassword='" + userPassword + '\'' +
", userEmail='" + userEmail + '\'' +
", userInfo='" + userInfo + '\'' +
", headImg=" + Arrays.toString(headImg) +
", createTime=" + createTime +
", role=" + role +
'}';
}
}
然后不要忘记再生成role相应的get()和set()方法,然后修改toString()方法,之前说过这些方法生成的快捷键,在此就不做代码的展示了。
使用自动映射其实就是通过别名让MyBatis自动将值匹配到对应的字段上,简单的别名映射就如我们之前使用过的user_name对应userName。除此之外,MyBatis还支持复杂的属性映射,可以多层嵌套,例如将role.role_name映射到role.roleName上。MyBatis会先查找role属性,如果存在role属性就创建role对象,然后在role对象中继续查找roleName,将role_name的值绑定到role对象的roleName属性上。根据自动映射的规则,我们首先在UserMapper接口类中新增一个方法selectUserAndRoleById(),之后我们在UserMapper.xml文件中增加该方法的SQL实现代码:
package Interface;
import pojo.User;
public interface UserMapper {
//根据用户Id查询用户信息和角色
User selectUserAndRoleById(Long userId);
}
我们已经假设过这种关系为一对一了,所以该方法的返回值我们定义为User类型。
<?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 namespace="Interface.UserMapper">
<!--根据用户ID查找用户信息和角色-->
<select id="selectUserAndRoleById" resultType="pojo.User">
select
u.id, u.user_name userName,u.user_email userEmail,
u.user_info userInfo, u,head_img headImg, u.create_time createTime,
r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled",
r.create_author "role.createAuthor", r.create_time "role.createTime"
from t_user u
inner join t_user_role ur on u.id = ur.user_id
inner join t_role r on ur.role_id = r.id
where u.id = #{userId}
</select>
</mapper>
我们上述方法查询t_role表中的列明的时候都是使用的"role."前缀,通过这种方法我们可以直接将role的属性映射到User实体的role属性上。然后我们添加测试方法。
@Test
public void testSelectUserAndRoleById() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//因为我们是假设的一对一的关系,与实际数据库表的关系并不符合,所以我们这里选择只有一个角色的用户Id
User user = userMapper.selectUserAndRoleById(1002L);
System.out.println(user);
} finally {
sqlSession.close();
}
}
执行结果符合我们预期。
像这种通过一次查询将结果映射到不同对象的方式,称之为关联的嵌套结果映射。关联的嵌套结果映射需要关联多个表将所有需要的值一次性查询出来。这种方式的好处是减少数据库查询次数,减轻数据库的压力,缺点是要写很复杂的SQL,并且当嵌套结果更复杂时,不容易一次写正确,由于要在应用服务器上将结果映射到不同的类上,因此也会增加应用服务器的压力。当一定会使用到嵌套结果,并且整个复杂的SQL执行速度很快时,建议使用关联的嵌套结果映射。
- 使用resultMap配置一对一映射
除了使用MyBatis的自动映射来处理一对一嵌套外,还可以在XML映射文件中配置结果映射。我们写一个新的方法来使用以下resultMap。
package Interface;
import pojo.User;
public interface UserMapper {
//根据用户Id查询用户信息和角色
User selectUserAndRoleById2(Long userId);
}
然后我们需要更改一下UserMapper.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 namespace="Interface.UserMapper">
<resultMap id="userRoleMap" type="pojo.User">
<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="userInfo" column="user_info"/>
<result property="headImg" column="head_Img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="role.id" column="role_id"/>
<result property="role.roleName" column="role_name"/>
<result property="role.enabled" column="enabled"/>
<result property="role.createAuthor" column="create_author"/>
<result property="role.createTime" column="role_create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<select id="selectUserAndRoleById2" resultMap = "userRoleMap">
select
u.id, u.user_name, u.user_email, u.user_info, u.head_img, u.create_time,
r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled",
r.create_author "role.createAuthor", r.create_time "role.createTime"
from t_user u
inner join t_user_role ur on u.id = ur.user_id
inner join t_role r on ur.role_id = r.id
where u.id = #{userId}
</select>
</mapper>
注意,因为这个方法使用了resultMap配置映射,所以返回值不能用resultType来设置,而是要使用resultMap属性将其配置为userRoleMap。测试方法和之前的一样:
@Test
public void testSelectUserAndRoleById2() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//因为我们是假设的一对一的关系,与实际数据库表的关系并不符合,所以我们这里选择只有一个角色的用户Id
User user = userMapper.selectUserAndRoleById2(1002L);
System.out.println(user);
} finally {
sqlSession.close();
}
}
其实上面的XML文件中的resultMap是可以继承的,因为我们在之前测试其他方法的时候已经写过了这个,所以我们可以直接继承以前使用过的userMap,那么代码就少了几行,变成了:
<?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 namespace="Interface.UserMapper">
<resultMap id="userMap" type="pojo.User">
<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="userInfo" column="user_info"/>
<result property="headImg" column="head_Img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<resultMap id="userRoleMap" extends = "userMap" type="pojo.User">
<result property="role.id" column="role_id"/>
<result property="role.roleName" column="role_name"/>
<result property="role.enabled" column="enabled"/>
<result property="role.createAuthor" column="create_author"/>
<result property="role.createTime" column="role_create_time" jdbcType="TIMESTAMP"/>
</resultMap>
</mapper>
- 使用resultMap的association标签配置一对一映射
在resultMap中,association标签用于和一个复杂的类型进行关联,在上面配置的基础上,我们再做一些修改,使用association标签的配置方式。
<?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 namespace="Interface.UserMapper">
<resultMap id="userMap" type="pojo.User">
<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="userInfo" column="user_info"/>
<result property="headImg" column="head_Img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<resultMap id="userRoleMap" extends="userMap" type="pojo.User">
<association property="role" columnPrefix="role_" javaType="pojo.Role">
<result property="role.id" column="role_id"/>
<result property="role.roleName" column="role_name"/>
<result property="role.enabled" column="enabled"/>
<result property="role.createAuthor" column="create_author"/>
<result property="role.createTime" column="role_create_time" jdbcType="TIMESTAMP"/>
</association>
</resultMap>
<select id="selectUserAndRoleById2" resultMap = "userRoleMap">
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.roleName", r.enabled "role_enabled",
r.create_author "role_createAuthor", r.create_time "role_createTime"
from t_user u
inner join t_user_role ur on u.id = ur.user_id
inner join t_role r on ur.role_id = r_id
where u.id = #{userId}
</select>
</mapper>
association标签包含以下属性:
- property:对应实体类中属性名,必填
- javaType:属性对应的Java类型
- resultMap:可以直接使用现有的resultMap,而不需要在这里配置
- columnPrefix:查询列的前缀,配置前缀后,在子标签配置result的column时可以省略前缀
因为我们在上面的XML文件中配置了属性role,因此在association内部配置result的property属性时直接按照Role对象中的属性名配置即可。另外我们还配置了columnPrefix="role_",在写SQL的时候,和t_role表相关的查询列的 列名都要有"role_"前缀,在内部result配置column时,需要配置成去掉前缀的列名,MyBatis在映射结果时会自动使用前缀和column值的组合去SQL查询的结果中取值。
现在其实我们一直是在UserMapper.xml文件中写的roleMap,其实我们也可以将其写在RoleMapper.xml中,但是要做些修改:
//仍然写在UserMapper.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 namespace="Interface.UserMapper">
<resultMap id="userMap" type="pojo.User">
<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="userInfo" column="user_info"/>
<result property="headImg" column="head_Img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
</mapper>
//写在RoleMapper.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 namespace="Interface.RoleMapper">
<resultMap id="roleMap" type="pojo.Role">
<result property="id" column="id"/>
<result property="roleName" column="role_name"/>
<result property="enabled" column="enabled"/>
<result property="createAuthor" column="create_author"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
</mapper>
//写在UserMapper.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 namespace="Interface.UserMapper">
<resultMap id="userRoleMap" extends="userMap" type="pojo.User">
<association property="role" columnPrefix="role_" resultMap="Interface.RoleMapper.roleMap"/>
</resultMap>
</mapper>
理解起来有些混乱,只需要记住其中一种方法就行。
- association标签的嵌套查询
除了以上几种方法外,我们还可以利用简单的SQL通过多次查询转换为我们需要的结果,这种方式与根据业务逻辑手动执行多次SQL的方式很像,最后会将结果组合成一个对象。
使用association标签的嵌套查询常用的属性有以下几个:
select:另一个映射查询的id,MyBatis会额外执行这个查询获取嵌套对象的结果
column:列名,将主查询中列的结果作为嵌套查询的参数,配置方式为:column = {prop1 = col1, prop2 = col2},prop1和prop2将作为嵌套查询的参数。
fetchType:数据加载方式,可选值为lazy和eager,分别为延迟加载和积极加载,这个配置会覆盖全局的lazyLoadingEnabled配置。
和之前一样,将userMap写在UserMapper.xml中,将roleMap写在RoleMapper.xml中,然后在UserMapper.xml中创建一个新的resultMap。然后新建一个查询方法,selectUserAndRoleById3(Long id),而且,还得记得在RoleMapper.xml中增加selectById()的方法代码。
package Interface;
import pojo.Role;
public interface RoleMapper {
Role selectRoleById(Long id);
}
//UserMapper.xml中的userMap
<?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 namespace="Interface.UserMapper">
<resultMap id="userMap" type="pojo.User">
<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="userInfo" column="user_info"/>
<result property="headImg" column="head_img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
</mapper>
//RoleMapper.xml中的roleMap
<?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 namespace="Interface.RoleMapper">
<resultMap id="roleMap" type="pojo.Role">
<result property="id" column="id"/>
<result property="roleName" column="role_name"/>
<result property="enabled" column="enabled"/>
<result property="createAuthor" column="create_author"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<select id="selectRoleById" resultMap="roleMap">
select * from t_role where id = #{id}
</select>
</mapper>
//UserMapper.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 namespace="Interface.UserMapper">
<resultMap id="userRoleMapSelect" extends="userMap" type="pojo.User">
<association property="role" column="{id=role_id}" select="Interface.RoleMapper.selectRoleById"/>
</resultMap>
<select id="selectUserAndRoleById3" resultMap = "userRoleMapSelect">
select
u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time, ur.role_id
from t_user u
inner join t_user_role ur on u.id = ur.user_id
where u.id = #{id}
</select>
</mapper>
测试代码和之前一样,只需要改一下测试的方法名即可,执行结果如下:
@Test
public void testSelectUserAndRoleById3() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//因为我们是假设的一对一的关系,与实际数据库表的关系并不符合,所以我们这里选择只有一个角色的用户Id
User user = userMapper.selectUserAndRoleById3(1002L);
System.out.println(user);
} finally {
sqlSession.close();
}
}
这是一个嵌套查询的SQL语句,先查询t_user表的信息,再查询t_role表的信息,因此执行了两次SQL。但是由于嵌套查询会多执行SQL,如果查询的结果是多条数据,那么主SQL查询一次,查询出N条结果,然后这N条结果要各自执行一次查询,有需要N次查询。那么association标签的fetchType属性就很好的解决了这个问题。我们需要把fetchType设置为lazy,这样的话,只有当调用getRole()方法获取role的时候,MyBatis才会执行嵌套查询去获取数据。具体操作方法不做展示,平时使用的时候会使用这几种一对一映射中的一种就行了。