在现实生活中,一对一关联关系是十分常见的。例如,一个人只能有一个身份证,同时一个身份证也只会对应一个人。
那么使用MyBatis是怎么处理这种一对一关联关系的呢?在前面讲解的<resultMap>元素中,包含了一
<association>子元素,MyBatis就是通过该元素来处理一对一关联关系的。
在<association>元素中,通常可以配置以下属性。
[if !supportLists]· [endif]property:指定映射到的实体类对象属性,与表字段一一对应。
[if !supportLists]· [endif]column:指定表中对应的字段。
[if !supportLists]· [endif]javaType:指定映射到实体对象属性的类型。
[if !supportLists]· [endif]select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询。
[if !supportLists]· [endif]fetchType:指定在关联查询时是否启用延迟加载。fetchType属性有lazy和eager两个属性值,默认值为lazy(即默认关联映射延迟加载)。
<association>元素的使用非常简单,只需要参考如下两种示例配置即可,具体如下。
1 <!-- 方式一:嵌套查询-->
2 <resultMap type="IdCard" id="IdCardById">
3 <id property="id" column="id" />
4 <result property="userName" column="userName" />
5 <!-- 一对一:association使用select属性引入另外一条SQL语句 -->
6 <association property="user" column="uid" javaType="User"
7 select="cn.dsscm.dao.UserMapper.findUserById" />
8 </resultMap>
9 <!-- 方式二:嵌套结果 -->
10 <resultMap type="IdCard" id="userRoleResult2">
11 <id property="id" column="id"/>
12 <result property="code" column="code"/>
13 <association property="user" javaType="User">
14 <id property="id" column="cid"/>
15 <result property="userName" column="userName" />
16 </association>
17 </resultMap>
MyBatis在映射文件中加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。嵌套查询是指通过执行另外一条SQL映射语句来返回预期的复杂类型;嵌套结果是使用嵌套结果映射来处理重复的联合结果的子集。开发人员可以使用上述任意一种方式实现对关联关系的加载。
示例:用户和身份证间关联
了解了MyBatis中处理一对一关联关系的元素和方式后,接下来就以用户和身份证之间的一对一关联关系为例,进行详细讲解。
查询个人及其关联的身份证信息是先通过查询个人表中的主键来获个人信息,然后通过表中的外键,来获取证件表中的身份证号信息。其具体实现步骤如下。
创建数据表,在dsscm数据库中重新创建名为tb_idcard的数据表,同时预先插入两条数据。其执行的SQL语句如下所示。
1 USE dsscm;
2 #创建一个名称为tb idcard 的表
3 CREATE TABLE `tb_idcard` (
4 `id` int(11) NOT NULL AUTO_INCREMENT,
5 `uid` int(11) NOT NULL,
6 `CODE` varchar(18) DEFAULT NULL,
7 PRIMARY KEY (`id`)
8 );
9 #插入n条数据
10 INSERT INTO tb_idcard(uid ,CODE) VALUES (1,'430101200001011234');
11 INSERT INTO tb_idcard(uid ,CODE) VALUES (2,'430101200001014321');
12 INSERT INTO tb_idcard(uid ,CODE) VALUES (3,'430101200001011235');
13 INSERT INTO tb_idcard(uid ,CODE) VALUES (4,'430101200001014326');
14 ......
完成上述操作后,数据库tb_idcard表中的数据。
在项目的cn.dsscm.pojo包下创建持久化类IdCard,编辑后的代码,如示例所示。
1 【示例1】 IdCard.java
2 public class IdCard {
3 private Integer id; // id
4 private Integer uid; // 用户id
5 private String code;// 身份证号码
6
7 private User user;// 一对一
8 //省略getter和setter方法
9 }
在上述示例中,分别定义了各自的属性以及对应的getter/setter方法,同时为了方便查看输出结果还重写了toString()方法。
在cn.dsscm.mapper包中,创建证件映射文件IdCardMapper.xml和用户映射文件UserMapper.xml,并在两个映射文件中编写一对一关联映射查询的配置信息,如示例所示。
1 【示例2】 IdCardMapper.xml
2 <?xml version="1.0" encoding="UTF-8"?>
3 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="cn.dsscm.dao.IdCardMapper">
6 <!-- 嵌套查询:通过执行另外一条SQL映射语句来返回预期的特殊类型 -->
7 <select id="findCodeById" parameterType="Integer" resultMap="IdCardById">
8 SELECT * FROM tb_idcard WHERE id=#{id}
9 </select>
10 <resultMap type="IdCard" id="IdCardById">
11 <id property="id" column="id" />
12 <result property="userName" column="userName" />
13 <!-- 一对一:association使用select属性引入另外一条SQL语句 -->
14 <association property="user" column="uid" javaType="User"
15 select="cn.dsscm.dao.UserMapper.findUserById" />
16 </resultMap>
17 </mapper>
1 【示例3】 UserMapper.xml
2 <?xml version="1.0" encoding="UTF-8"?>
3 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="cn.dsscm.dao.UserMapper">
6 <!-- 根据id查询用户信息 -->
7 <select id="findUserById" parameterType="Integer" resultType="User">
8 SELECT * from tb_user where id=#{id}
9 </select>
10 </mapper>
在上述两个映射文件中,使用了MyBatis中的嵌套查询方式进行了个人及其关联的证件信息查询,因为返回的个人对象中除了基本属性外还有一个关联的uid属性,所以需要手动编写结果映射。从映射文件IdCardMapper.xml中可以看出,嵌套查询的方法是先执行一个简单的SQL语句,然后在进行结果映射时,将关联对象在<association>元素中使用select属性执行另一条SQL语句(即 IdCardMapper.xml中的SQL )。
创建映射文件的对应接口如下:
1 public List<IdCard> findCodeById(@Param("id")Integer id);
在核心配置文件mybatis-config.xml中,引入Mapper映射文件并定义别名,如下所示。
1 【示例4】 mybatis-config.xml
2 <!-- 将mapper文件加入到配置文件中 -->
3 <mappers>
4 <mapper resource="cn/dsscm/dao/UserMapper.xml" />
5 <mapper resource="cn/dsscm/dao/IdCardMapper.xml" />
6 </mappers>
在上述核心配置文件中,首先引入了数据库连接的配置文件,然后使用扫描包的形式自定义别名,接下来进行环境的配置,最后配置了Mapper映射文件的位置信息。
在测试包中,创建测试类UserMapperTest,并在类中编写测试方法getUserListByIdTest (),如下所示。
1 【示例5】 UserMapperTest.java
2 @Test
3 public void getUserListByIdTest(){
4 SqlSession sqlSession = null;
5 List<IdCard> userList = new ArrayList<IdCard>();
6 Integer id = 3;
7 try {
8 sqlSession = MyBatisUtils.createSqlSession();
9 userList = sqlSession.getMapper(IdCardMapper.class).findCodeById(id);
10 } catch (Exception e) {
11 // TODO: handle exception
12 e.printStackTrace();
13 }finally{
14 MyBatisUtils.closeSqlSession(sqlSession);
15 }
16
17 logger.debug("getUserListByRoleIdTest userList.size : " + userList.size());
18 for(IdCard user:userList){
19 logger.debug(user);
20 }
21 }
在getUserListByIdTest()方法中,首先通过MybatisUtils工具类获取了SqlSession对象,然后通过SqlSession对象的接口方法获取了用户信息。为了查看结果,这里使用了输出语句输出查询结果信息。最后程序执行完毕时,关闭了SqlSession。
使用JUnit4执行getUserListByIdTest()方法后,控制台的输出结果如下:
1 cn.dsscm.dao.IdCardMapper.findCodeById - ==> Preparing: SELECT * FROM tb_idcard WHERE id=?
2 cn.dsscm.dao.IdCardMapper.findCodeById - ==> Parameters: 3(Integer)
3 ......
4 cn.dsscm.dao.UserMapper.findUserById - ==> Preparing: SELECT * from tb_user where id=?
5 cn.dsscm.dao.UserMapper.findUserById - ==> Parameters: 3(Integer)
6 ......
7 cn.dsscm.test.UserMapperTest - getUserListByRoleIdTest userList.size : 1
8 cn.dsscm.test.UserMapperTest - IdCard [id=3, uid=null, code=430101200001011235, user=User [id=3, userCode=zhangwei, userName=张伟,userPassword=0000000, birthday=Sun Jun 05 00:00:00 CST 1994, gender=2, phone=18567542321, email=null, address=北京市朝阳区,userDesc=null, userRole=2, createdBy=1, imgPath=null, creationDate=Thu Oct 24 13:01:51 CST 2019, modifyBy=null, modifyDate=null,age=null, userRoleName=null]]
从控制台的输出结果可以看出,使用MyBatis嵌套查询的方式查询出了用户身份证信息及其用户的信息,这就是MyBatis中的一对一关联查询。
修改代码,使用MyBatis嵌套结果的方式查询出了用户身份证信息及其用户的信息。
创建映射文件的对应接口如下:
1 public List<IdCard> findCodeById2(@Param("uid")Integer id);
在cn.dsscm.mapper包中,修改证件映射文件IdCardMapper.xml,并在映射文件中使用MyBatis嵌套结果编写一对一关联映射查询的配置信息,如示例所示。
1 【示例6】 IdCardMapper.xml
2 <!-- 根据roleId获取用户列表 association start-->
3 <resultMap type="IdCard" id="userRoleResult2">
4 <id property="id" column="id"/>
5 <result property="code" column="code"/>
6 <association property="user" javaType="User">
7 <id property="id" column="cid"/>
8 <result property="userName" column="userName" />
9 </association>
10 </resultMap>
11 <select id="findCodeById2" parameterType="Integer" resultMap="userRoleResult2">
12 SELECT u.* ,c.id cid ,c.code
13 FROM tb_user u, tb_idcard c
14 WHERE u.id=c.uid
15 AND c.uid= #{uid}
16 </select>
在测试包中,在类中编写测试方法getUserListByIdTest2(),如下所示。
1 【示例7】 UserMapperTest.java
2 @Test
3 public void getUserListByIdTest2(){
4 SqlSession sqlSession = null;
5 List<IdCard> userList = new ArrayList<IdCard>();
6 Integer id = 3;
7 try {
8 sqlSession = MyBatisUtils.createSqlSession();
9 userList = sqlSession.getMapper(IdCardMapper.class).findCodeById2(id);
10 } catch (Exception e) {
11 // TODO: handle exception
12 e.printStackTrace();
13 }finally{
14 MyBatisUtils.closeSqlSession(sqlSession);
15 }
16 logger.debug("getUserListByRoleIdTest userList.size : " + userList.size());
17 for(IdCard user:userList){
18 logger.debug(user);
19 }
20 }
使用JUnit4执行getUserListByIdTest2()方法后,控制台的输出结果如下:
1 cn.dsscm.dao.IdCardMapper.findCodeById2 - ==> Preparing: SELECT u.* ,c.id cid ,c.code FROM tb_user u, tb_idcard c WHERE u.id=c.uid AND c.uid= ?
2 cn.dsscm.dao.IdCardMapper.findCodeById2 - ==> Parameters: 3(Integer)
3 ......
4 cn.dsscm.test.UserMapperTest - getUserListByRoleIdTest userList.size : 1
5 cn.dsscm.test.UserMapperTest - IdCard [id=3, uid=null, code=430101200001011235, user=User [id=3, userCode=null, userName=张伟, userPassword=null, birthday=null, gender=null, phone=null, email=null, address=null, userDesc=null, userRole=null, createdBy=null, imgPath=null, creationDate=null, modifyBy=null, modifyDate=null, age=null, userRoleName=null]]
上述示例使用身份证类关联用户信息,改变实体类用用户类关联身份证类也可以实现同样效果,此处不再赘述。
虽然使用嵌套查询的方式比较简单,但是从控制台的输出结果中可以看出,MyBatis嵌套查询的方式要执行多条SQL语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的SQL语句被执行,从而极大地消耗数据库性能并且会降低查询效率。这并不是开发人员所期望的。为此,我们可以使用MyBatis提供的嵌套结果方式,来进行关联查询。
示例:用户和用户角色关联
association:映射到JavaBean的某个"复杂类型"属性,比如JavaBean类,即JavaBean内部嵌套一个复杂数据类型(JavaBean)属性,这种情况就属于复杂类型的关联。但是需要注意:association仅处理一对一的关联关系。
在实际的开发项目中此类绝对的双向的一对一的关联比较少见,很多时候是单向的。比如用户角色和用户列表关系,从不同角度看映射关系不一样,这里面涉及用户表(tb_user)和用户权限表(tb_role),从用户角度关联权限信息这是一对一,从用户权限关联用户信息是一对多。如果根据用户角色id获取该角色下的用户列表的情况,我们只需要根据用户表关联用户角色表,association便可以处理此种情况下一对一的关联关系,那么对于用户角色关联用户信息的一对多的关联关系的处理,则需要collection元素来实现了,这个后面介绍。
创建数据表,在dsscm数据库中重新创建名为tb_idcard的数据表,同时预先插入两条数据。其执行的SQL语句如下所示。
1 #用户表
2 CREATE TABLE `tb_user` (
3 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
4 `userCode` varchar(15) NOT NULL COMMENT '用户编码',
5 `userName` varchar(15) NOT NULL COMMENT '用户名称',
6 `userPassword` varchar(15) NOT NULL COMMENT '用户密码',
7 `gender` int(10) DEFAULT NULL COMMENT '性别(1:女、 2:男)',
8 `birthday` date DEFAULT NULL COMMENT '出生日期',
9 `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
10 `phone` varchar(15) COMMENT '手机',
11 `address` varchar(30) COMMENT '地址',
12 `userDesc` text COMMENT '简介',
13 `userRole` int(10) DEFAULT NULL COMMENT '用户角色(取自角色表-角色id)',
14 `imgPath` varchar(100) DEFAULT NULL COMMENT '用户照片',
15 `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者(userId)',
16 `creationDate` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
17 `modifyBy` bigint(20) DEFAULT NULL COMMENT '更新者(userId)',
18 `modifyDate` datetime DEFAULT NULL COMMENT '更新时间',
19 PRIMARY KEY (`id`),
20 UNIQUE KEY `userCode` (`userCode`)
21 );
22
23 #用户权限表
24 CREATE TABLE `tb_role` (
25 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
26 `roleCode` varchar(50) NOT NULL COMMENT '角色编码',
27 `roleName` varchar(50) NOT NULL COMMENT '角色名称',
28 `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者',
29 `creationDate` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
30 `modifyBy` bigint(20) DEFAULT NULL COMMENT '修改者',
31 `modifyDate` datetime DEFAULT NULL COMMENT '修改时间',
32 PRIMARY KEY (`id`),
33 UNIQUE KEY `roleCode` (`roleCode`)
34 );
35
首先创建Role类,并增加相应的getter和setter方法,示例代码如下:
1 【示例8】 Role.java
2 public class Role {
3 private Integer id; // id
4 private String roleCode; // 角色编码
5 private String roleName; // 角色名称
6 private Integer createdBy; // 创建者
7 private Date creationDate; // 创建时间
8 private Integer modifyBy; // 更新者
9 private Date modifyDate;// 更新时间
10 //省略getter和setter方法
11 }
修改User类,增加角色属性(Role role),并增加相应的getter和setter方法;注释掉用户角色名称属性(String userRoleName),并注释掉其getter和setter方法,示例代码如下:
1 【示例9】 User.java
2 public class User {
3 private Integer id; //id
4 private String userCode; //用户编码
5 private String userName; //用户名称
6 private String userPassword; //用户密码
7 private Integer gender; //性别
8 private Date birthday; //出生日期
9 private String phone; //电话
10 private String address; //地址
11 private Integer userRole; //用户角色ID
12 private Integer createdBy; //创建者
13 private Date creationDate; //创建时间
14 private Integer modifyBy; //更新者
15 private Date modifyDate; //更新时间
16
17 private Integer age;//年龄
18 //private String userRoleName; //用户角色名称
19
20 //association
21 private Role role; //用户角色
22
23 //省略getter和setter方法
24 }
通过以上改造,我们的JavaBean:User对象内部嵌套了一个复杂数据类型的属性:role。接下来在UserMapper接口里增加根据角色id获取用户列表的方法,代码如下:
1 public List<User> getUserListByRoleId(@Param("userRole")Integer roleId);
修改对应UserMapper.xml,增加getUserListByRoleId,该select查询语句返回类型为resultMap,并且外部引用的resultMap的类型为User。由于User对象内嵌JavaBean对象(Role),因此需要使用association来实现结果映射。代码如下:
1 【示例10】 UserMapper.xml
2 <!-- 根据roleId获取用户列表 association start-->
3 <resultMap type="User" id="userRoleResult">
4 <id property="id" column="id"/>
5 <result property="userCode" column="userCode" />
6 <result property="userName" column="userName" />
7 <result property="userRole" column="userRole" />
8 <association property="role" javaType="Role">
9 <id property="id" column="r_id"/>
10 <result property="roleCode" column="roleCode"/>
11 <result property="roleName" column="roleName"/>
12 </association>
13 </resultMap>
14 <select id="getUserListByRoleId" parameterType="Integer" resultMap="userRoleResult">
15 select u.*,r.id as r_id,r.roleCode,r.roleName
16 from tb_user u,tb_role r
17 where u.userRole = #{userRole} and u.userRole = r.id
18 </select>
从上述代码,简单分析association的属性。
[if !supportLists]· [endif]javaType:完整Java类名或者别名。若映射到一个JavaBean,则MyBatis通常会自行检测到其类型;若映射到一个HashMap,则应该明确指定javaType,来确保所需行为。此处为Role。
[if !supportLists]· [endif]property :映射数据库列的实体对象的属性。此处为在User里定义的属性:role。
[if !supportLists]· [endif]association的子元素如下: id; result;property:映射数据库列的实体对象的属性。此处为Role的属性; column:数据库列名或别名。
在做结果映射的过程中,需要注意:要确保所有的列名都是唯一且无歧义的。id子元素在嵌套结果映射中扮演了非常重要的角色,应该指定一个或者多个属性来唯一标识这个结果集。实际上,即便没有指定id,MyBatis也会工作,但是会导致严重的性能开销,所以最好选择尽量少的属性来唯一标识结果,主键或者联合主键均可。
最后修改测试类UserMapperTest.java,增加测试方法,示例代码如下:
1 【示例11】 UserMapperTest.java
2 @Test
3 public void getUserListByRoleIdTest(){
4 SqlSession sqlSession = null;
5 List<User> userList = new ArrayList<User>();
6 Integer roleId = 3;
7 try {
8 sqlSession = MyBatisUtils.createSqlSession();
9 userList = sqlSession.getMapper(UserMapper.class).getUserListByRoleId(roleId);
10 } catch (Exception e) {
11 // TODO: handle exception
12 e.printStackTrace();
13 }finally{
14 MyBatisUtils.closeSqlSession(sqlSession);
15 }
16
17 logger.debug("getUserListByRoleIdTest userList.size : " + userList.size());
18 for(User user:userList){
19 logger.debug("userList =====> userName: " + user.getUserName()
20 +", <未做映射字段>userPassworD. " + user.getUserPassword()
21 + ", Role: " + user.getRole().getId() + " --- "
22 + user.getRole().getRoleCode() +" --- " + user.getRole().getRoleName());
23 }
24 }
在测试方法中调用getUserListByRoleId()方法获取userList,并进行结果输出,关键是映射的用户角色相关信息。
1 cn.dsscm.dao.UserMapper.getUserListByRoleId - ==> Preparing: select u.*,r.id as r_id, r.roleCode ,r.roleName from tb_user u,tb_role r where u.userRole = ? and u.userRole = r.id
2 cn.dsscm.dao.UserMapper.getUserListByRoleId - ==> Parameters: 3(Integer)
3 ......
4 cn.dsscm.test.UserMapperTest - getUserListByRol