4.2 一对一(association)
在现实生活中,一对一关联关系是十分常见的。如一个人只能有一个身份证,同时一个身份证也只会对应一个人,它们之间的关系模型如图4.3所示。
图4.3 人与身份证的关联关系
那么使用MyBatis框架是如何处理这种一对一关联关系的呢?在前面讲解的<resultMap>元素中,包含了一个<association>子元素,MyBatis框架就是通过该子元素来处理一对一关联关系的。
在<association>元素中,通常可以配置以下属性。
(1)property:指定映射到的实体类对象属性,与表字段一一对应。
(2)column:指定表中对应的字段。
(3)javaType:指定映射到实体对象属性的类型。
(4)select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询。
(5)fetchType:指定在关联查询时是否启用延迟加载。fetchType属性有lazy和eager两个属性值,默认值为lazy(默认关联映射延迟加载)。
<association>元素的使用非常简单,只需要参考如下两种示例配置即可,具体如下。
MyBatis在映射文件中加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。嵌套查询是指通过执行另外一条SQL映射语句来返回预期的复杂类型;嵌套结果是使用嵌套结果映射来处理重复的联合结果的子集。开发人员可以使用上述任意一种方式实现对关联关系的加载。
4.2.1 应用案例:用户和身份证间的关联
了解MyBatis中处理一对一关联关系的元素和方式后,接下来就以用户和身份证之间的一对一关联关系为例进行详细讲解。
查询个人及其关联的身份证信息可先通过查询个人表中的主键来获得个人信息,然后通过表中的外键来获取证件表中的身份证号信息,其具体实现步骤如下。
创建数据表,在dsscm数据库中重新创建名为tb_idcard的数据表,同时预先插入两条数据,其执行的SQL语句如下所示:
完成上述操作后,数据库tb_idcard表中的数据如图4.4所示。
图4.4 tb_idcard表
在项目cn.dsscm.pojo包下创建持久化类IdCard,编辑后的代码,见示例1。
【示例1】 IdCard.java
在上述示例中,分别定义了各自的属性及对应的getter方法和setter方法,同时为了方便查看输出结果还重写了toString()方法。
在cn.dsscm.mapper包中,创建证件映射文件IdCardMapper.xml和用户映射文件UserMapper.xml,并在两个映射文件中编写一对一关联映射查询的配置信息,见示例2。
【示例2】 IdCardMapper.xml
【示例3】 UserMapper.xml
在上述两个映射文件中,使用MyBatis框架中的嵌套查询方式进行了个人及其关联的证件信息查询,因为返回的个人对象中除了基本属性还有一个关联的uid属性,所以需要手动编写结果映射。从映射文件IdCardMapper.xml中可以看出,嵌套查询的方法是先执行一个简单的SQL语句,然后在进行结果映射时,将关联对象在<association>元素中使用select属性执行另一条SQL语句(IdCardMapper.xml中的SQL)。
创建映射文件的对应接口如下:
在核心配置文件mybatis-config.xml中,引入Mapper映射文件并定义别名,见示例4。
【示例4】 mybatis-config.xml
在上述核心配置文件中,先引入数据库连接的配置文件,然后使用扫描包的形式自定义别名,接下来进行环境的配置,最后配置Mapper映射文件的位置信息。
在测试包中,创建测试类UserMapperTest,并在类中编写测试方法getUserListByIdTest (),见示例5。
【示例5】 UserMapperTest.java
在getUserListByIdTest()方法中,先通过MybatisUtils工具类获取SqlSession对象,然后通过SqlSession对象的接口方法获取用户信息,并使用输出语句查询结果信息,最后程序执行完毕时,关闭SqlSession对象。
使用JUnit4执行getUserListByIdTest()方法后,控制台的输出结果如下:
从控制台的输出结果可以看出,使用MyBatis框架嵌套方式查询出了用户身份证信息及其用户的信息,这就是MyBatis框架中的一对一关联查询。
修改代码可使用MyBatis框架嵌套结果的方式查询用户身份证信息及其用户的信息。
创建映射文件的对应接口如下:
在cn.dsscm.mapper包中,修改证件映射文件IdCardMapper.xml,并在映射文件中使用MyBatis框架嵌套结果编写一对一关联映射查询的配置信息,见示例6。
【示例6】 IdCardMapper.xml
在测试包的类中编写测试方法getUserListByIdTest2(),见示例7。
【示例7】 UserMapperTest.java
使用JUnit4执行getUserListByIdTest2()方法后,控制台的输出结果如下:
上述示例使用身份证类关联用户信息,改变实体类时使用用户类关联身份证类也可以实现同样的效果,此处不再赘述。
4.2.2 应用案例:用户和用户角色的关联
使用association映射到JavaBean的某个“复杂类型”属性,如JavaBean类,即JavaBean内部嵌套一个复杂数据类型(JavaBean)属性,这种情况就属于复杂类型的关联。但是需要注意的是,association元素仅处理一对一的关联关系。
在实际的开发项目中这类绝对的双向一对一关联比较少见,很多是单向的。如用户角色和用户列表关系,从不同角度看映射关系不一样,这里涉及用户表(tb_user)和用户权限表(tb_role),从用户角度关联权限信息是一对一的,从用户权限关联用户信息是一对多的。如果根据用户角色id获取该角色用户列表的情况,只需要根据用户表关联用户角色表即可,association便可以处理此种情况下的一对一关联关系,那么对于用户角色关联用户信息的一对多的关联关系的处理,则需要使用collection来实现了,这部分内容将在后面介绍。
先创建role类,并增加相应的getter方法和setter方法,见示例8。
【示例8】 Role.java
修改User类,以增加角色属性(Role role),并增加相应的getter方法和setter方法;注释掉用户角色名称属性(String userRoleName),及其getter方法和setter方法,见示例9。
【示例9】 User.java
通过以上改造,在JavaBean User对象内部嵌套了一个复杂数据类型的属性(role)。接下来在UserMapper接口里增加根据角色id获取用户列表的方法,代码如下:
修改对应UserMapper.xml,以增加getUserListByRoleId,该select查询语句返回类型为resultMap,并且外部引用的resultMap类型为User。由于User内嵌了JavaBean对象(role),因此需要使用association来实现结果映射,见示例10。
【示例10】 UserMapper.xml
从上述代码,简单分析association的属性。
(1)javaType:指完整Java类名或者别名。若映射到一个JavaBean时,则MyBatis通常会自行检测其类型;若映射到一个HashMap时,则应该明确指定javaType来确保所需行为。此处为role。
(2)property:指映射数据库列的实体对象的属性。此处为在User里定义的属性(role)。
(3)association的子元素如下。
①id:映射数据库中表主键字段。
②result:映射数据库中表字段。
③property:映射数据库列的实体对象的属性。此处为role的属性。
④column:数据库列名或别名。
在做结果映射的过程中,需要注意的是,要确保所有的列名都是唯一且无歧义。id子元素在嵌套结果映射中扮演了非常重要的角色,应该指定一个或者多个属性来唯一标识这个结果集。实际上,即便没有指定id,MyBatis框架也会工作,但是会导致严重的性能开销,所以最好选择尽量少的属性来唯一标识结果,主键或者联合主键均可。
最后修改测试类UserMapperTest.java,以增加测试方法,见示例11。
【示例11】 UserMapperTest.java
在测试方法中调用getUserListByRoleId()方法获取userList,并进行结果输出,关键是映射的用户角色相关信息。
通过以上示例可了解关于association的基本用法及适用场景,那么现在再思考一个问题:通过使用“userRoleResult”联合一个association的结果映射来加载User实例,那么association的role结果映射是否可复用呢?
答案是肯定的,association还提供了另一个属性—resultMap。通过这个属性可以扩展一个resultMap来进行联合映射,这样就可以使role结果映射重复使用。当然,若不需要重用,也可按照之前的写法,直接嵌套这个联合结果映射,应根据具体业务而定。下面就使用resultMap完成association的role映射结果的复用。具体操作如下。
修改UserMapper.xml,以增加resultMap来完成role的结果映射,使association增加属性resultMap来引用外部的“roleResult”,见示例12。
【示例12】 UserMapper.xml
在上述代码中,把之前的角色结果映射代码抽取出来放在一个resultMap中,然后设置了association的resultMap属性来引用外部的“roleResult”。这样做的好处就是可以达到复用的效果,并且整体的结构较为清晰明了,特别适合association的结果映射比较多的情况。
运行结果如下:
从控制台的输出结果可以看出,使用MyBatis框架嵌套查询的方式查询出用户及其权限的信息,这就是MyBatis中的一对一关联查询。
虽然使用嵌套查询的方式比较简单,但是从控制台的输出结果中可以看出,MyBatis嵌套查询的方式要执行多条SQL语句,这对于大型数据集合和列表展示不是很理想,因为这样可能会导致成百上千条关联的SQL语句被执行,从而极大地消耗数据库性能并且会降低查询效率,这并不是开发人员所期望的。为此,可以使用MyBatis提供的嵌套结果方式来进行关联查询。
在PersonMapper.xml中,可使用MyBatis框架嵌套结果的方式进行个人及其关联的证件信息查询,所添加的代码见示例13。
【示例13】 UserMapper.xml
从上述代码中可以看出,MyBatis框架嵌套结果的方式只编写了一条复杂的多表关联的SQL语句,并且在<association>元素中继续使用相关子元素进行数据库表字段和实体类属性的一一映射。
在测试类中编写测试方法getUserListByRoleIdTest3(),其代码见示例14。
【示例14】 UserMapperTest.java
使用JUnit4执行getUserListByRoleIdTest3()方法后,控制台的输出结果如下:
从控制台的输出结果可以看出,使用MyBatis嵌套结果的方式只执行了一条SQL语句,并且同样查询出了个人及其关联的身份证的信息。
经验:在使用MyBatis框架嵌套查询方式进行关联查询映射时,其延迟加载在一定程度上可以降低运行消耗并提高查询效率。MyBatis框架默认没有开启延迟加载,需要在核心配置文件mybatis-config.xml的<settings>元素内进行配置,具体配置方式如下。
在映射文件中,MyBatis框架关联映射的<association>元素和<collection>元素都已默认配置了延迟加载属性,即默认属性fetchType="lazy"(立即加栽),所以在配置文件中开启延迟加载后,无须在映射文件中再做配置。
4.2.3 技能训练
上机练习1 实现采购订单表的查询(association)
需求说明
(1)在上机练习的基础上,实现按条件查询订单表:
①商品名称(模糊查询)。
②供应商(供应商id)。
③是否付款。
(2)查询结果列显示:订单编码、商品名称、供应商编码、供应商名称、供应商联系人、供应商联系电话、订单金额、是否付款。
(3)resultMap中使用association子元素完成内部嵌套。
(1)修改Bill类,以增加复杂类型属性:Provider provider。
(2)编写SQL查询语句(连表查询)。
(3)创建resultMap自定义映射结果,并在select中引用。