通过前面几章的学习,大家已经熟悉了 MyBatis 的基本知识,并能够使用 MyBatis 以及面向对象的方式进行数据库操作,但这些操作只是针对单表实现的。 在实际的开发中,对数据库的操作常常会涉及多张表,这在面向对象中就涉及了对象与对象之间的关联关系。 针对多表之间的操作, MyBatis 提供了关联映射,通过关联映射就可以很好地处理对象与对象之间的关联关系。 本章中,将对 MyBatis 的关联关系映射进行详细讲解。
关联关系概述
在关系型数据库中,多表之间存在着三种关联关系,分别为一对一 、 一对多和多对多, 如图所示。
这三种关联关系的具体说明如下。
- 一对一:在任意一方引入对方主键作为外键。
- 一对多:在"多"的一方,添加"一"的一方的主键作为外键。
- 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键或使用新 的宇段作为主键。
通过数据库中的表可以描述数据之间的关系,同样,在 Java 中,通过对象也可以进行关系描述,如图所示。
在图中,三种关联关系的描述如下。
- 一对一的关系:就是在本类中定义对方类型的对象,如 A 类中定义 B 类类型的属性 b , B 类中定义 A 类类型的属性 a。
- 一对多的关系:就是一个 A 类类型对应多个 B 类类型的情况,需要在 A 类中以集合的方 式51入 B 类类型的对象,在 B 类中定义 A 类类型的属性 a。
- 多对多的关系:在 A 类中定义 B 类类型的集合,在 B 类中定义 A 类类型的集合。
以上就是 Java 对象中,三种实体类之间的关联关系,那么使用 MyBatis 是如何处理 Java 对象中的三种关联关系呢?在接下来的 3 个小节中,将对 MyBatis 中的这几种关联关系的使用进行详细讲解。
一对一
在现实生活中,一对一关联关系是十分常见的。 例如,一个人只能有一个身份证,同时一个身份证也只会对应一个人,它们之间的关系模型图,如图所示。
那么使用 MyBatis 是怎么处理图中的这种一对一关联关系的呢?在前面第 7 章所讲解的
元素中,包含了一个 子元素, MyBatis 就是通过该元素来处理一对 一关联关系的。
在元素中,通常可以配置以下属性。
- property: 指定映射到的实体类对象属性,与表字段一一对应。
- column: 指定表中对应的字段。
- javaType: 指定映射到实体对象属性的类型。
- select: 指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。
- fetchType: 指定在关联查询时是否启用延迟加载。 fetchType 属性有 lazy 和 eager 两个属性值,默认值为 lazy (即默认关联映射延迟加载)。
元素的使用非常简单,只需要参考如下两种示例配置即可,具体如下。
小提示:MyBatis 在映射文件中加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。 嵌套查询是指通过执行另外一条 SQL 映射语句来返回预期的复杂类型;嵌套结果是使用嵌套结果映射 来处理重复的联合结果的子集。 开发人员可以使用上述任意一种方式实现对关联关系的加载。
了解了 MyBatis 中处理一对一关联关系的元素和方式后,接下来就以个人和身份证之间的一 对一关联关系为例,进行详细讲解。
查询个人及其关联的身份证信息是先通过查询个人表中的主键来获个人信息,然后通过表中的外键,来获取证件表中的身份证号信息。 其具体实现步骤如下。
( 1 )创建数据表。 在 mybatis 数据库中分别创建名为 tb_idcard 和 tb_person 的数据表,同时预先插入两条数据。 完成上述操作后,数据库 tb_idcard 和 tb_person 表中的数据如图所示。
( 2 )在 Eclipse 中创建一个名为 mybatis03 的 Web 项目,然后引入相关 JAR 包、 log4j 曰志文件、 MybatisUtils 工具类以及 mybatis-config.xml 核心配置文件。 项目环境搭建完成后的文件结构,如图所示。
( 3 )在项目的 com.neuedu.po 包下创建持久化类 IdCard 和 Person ,编辑后的代码,文件如下所示。package com.neuedu.po; /** * 证件持久化类 */ public class IdCard { private Integer id; private String code; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } @Override public String toString() { return "IdCard [id=" + id + ", code=" + code + "]"; } }
package com.neuedu.po; /** * 个人持久化类 */ public class Person { private Integer id; private String name; private Integer age; private String sex; private IdCard card; //个人关联的证件 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public IdCard getCard() { return card; } public void setCard(IdCard card) { this.card = card; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + ", age=" + age + ", sex=" + sex + ", card=" + card + "]"; } }
在上述两个文件中,分别定义了各自的属性以及对应的 getter/setter 方法,同时为了方便查看输出结果还重写了 toString()方法。
(4 )在 com.neuedu.mapper 包中,创建证件映射文件 IdCardMapper.xml 和个人映射文件 PersonMapper.xml ,并在两个映射文件中编写一对一关联映射查询的配置信息,文件如下所示。
在上述两个映射文件中,使用了 MyBatis 中的嵌套查询方式进行了个人及其关联的证件信息查询,因为返回的个人对象中除了基本属性外还有一个关联的 card 属性,所以需要手动编写结果映射。 从映射文件 PersonMapper.xml 中可以看出,嵌套查询的方法是先执行一个简单的 SOL 语句,然后在进行结果映射时,将关联对象在
元素中使用 select 属性执行另一条 SOL 语句(即 IdCardMapper.xml 中的 SOL)。
( 5 )在核心配置文件 mybatis-config.xml 中,引入 Mapper 映射文件并定义别名,文件如下所示。
在上述核心配置文件中,首先引入了数据库连接的配置文件,然后使用扫描包的形式自定义别名,接下来进行环境的配置,最后配置了 Mapper 映射文件的位置信息。
( 6 )在 com.neuedu.test 包中,创建测试类 MybatisAssociatedTest ,并在类中编写测试方法 findPersonByldTest() ,文件如下所示。package com.neuedu.test; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import com.neuedu.po.Person; import com.neuedu.utils.MybatisUtils; /** * MyBatis 关联查询映射测试类 */ public class MybatisAssociatedTest { /** * 嵌套查询 */ @Test public void findPersonByldTest(){ // 1.通过工具类生成SqlSession对象 SqlSession sqlSession = MybatisUtils.getSession(); // 2.使用MyBatis 嵌套查询的方式查询id为1的人的信息 Person person = sqlSession.selectOne("com.neuedu.mapper.PersonMapper.findPersonById",1); // 3.输出查询结果信息 System.out.println(person); // 4.关闭SQlSession sqlSession.close(); } }
在文件中的 findPersonByldTest() 方法中,首先通过 MybatisUtils 工具类获取了 SqlSession 对象,然后通过 SqlSession 对象的 selectOne() 方法获取了个人信息。 为了查看结果, 这里使用了输出语句输出查询结果信息。 最后程序执行完毕时,关闭了 SqlSession。
使用 JUnit4 执行 findPersonByldTest() 方法后,控制台的输出结果如图所示。
从图中可以看出,使用 MyBatis 嵌套查询的方式查询出了个人及其关联的身份证信息, 这就是 MyBatis 中的一对一关联查询。
虽然使用嵌套查询的方式比较简单,但是从图中可以看出, MyBatis 嵌套查询的方式要执行多条 SOL 语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的 SOL 语句被执行,从而极大地消耗数据库性能并且会降低查询效率。 这并不是开发人员所期望的。 为此,我们可以使用 MyBatis 提供的嵌套结果方式,来进行关联查询。
在 PersonMapper.xml 中,使用 MyBatis 嵌套结果的方式进行个人及其关联的证件信息查询, 所添加的代码如下所示。
从上述代码中可以看出, MyBatis 嵌套结果的方式只编写了一条复杂的多表关联的 SOL 语句, 并且在
元素中继续使用相关子元素进行数据库表字段和实体类属性的一一映射。
在测试类 MybatisAssociatedTest 中,编写测试方法 findPersonByldTest2() ,其代码如下所示。/** * 嵌套结果 */ @Test public void findPersonByld2Test(){ // 1.通过工具类生成SqlSession对象 SqlSession sqlSession = MybatisUtils.getSession(); // 2.使用MyBatis 嵌套查询的方式查询id为1的人的信息 Person person = sqlSession.selectOne("com.neuedu.mapper.PersonMapper.findPersonById2",1); // 3.输出查询结果信息 System.out.println(person); // 4.关闭SQlSession sqlSession.close(); }
使用 JUnit4 执行 findPersonByldTest() 方法后,控制台的输出结果如图所示。
从图中可以看出,使用 MyBatis 嵌套结果的方式只执行了一条 SQL 语句,并且同样查询出了个人及其关联的身份证的信息。
- 多学一招: MyBatis延迟加载的配置
在使用 MyBatis 换套查询方式进行 MyBatis 关联查询映射时,使用 MyBatis 的延迟加载在一 定程度上可以降低运行消耗并提高查询效率。 MyBatis 默认没有开启延迅加载,需要在核心配置文件 mybatis-config.xml 中的元素内进行配置,具体配置方式如下。
在映射文件中, MyBatis 关联映射的
元索和 元素中都已默认配置了延迟加载属性, 即默认属性 fetchType="lazy" (属性 fetchType="eager"表示立即加载),所以在配置文件中开启延迟加载后,无须在映射文件中再做配置。
一对多
与一对一的关联关系相比,开发人员接触更多的关联关系是一对多(或多对一)。 例如一个用户可以有多个订单,同时多个订单归一个用户所有。 用户和订单的关联关系如图所示。
那么使用 MyBatis 是怎么处理这种一对多关联关系的呢?在前面第 7 章所讲解的
元素中,包含了一个 子元素, MyBatis 就是通过该元素来处理一对多关联关系的。 子元素的属性大部分与 元素相同,但其还包含一个特殊属性一一ofType。 ofType 属性与 javaType 属性对应,它用于指定实体对象中集合类属性所包含的元素类型。
元素的使用也非常简单,同样可以参考如下两种示例进行配置,具体代码如下。
在了解了 MyBatis 处理一对多关联关系的元素和方式后,接下来以用户和订单之间的这种一对多关联关系为例,详细讲解如何在 MyBatis 中处理一对多关联关系,具体步骤如下。
( 1 )在 mybatis 数据库中,创建两个数据表,分别为 tb_user 和 tb_orders ,同时在表中预先插入几条数据,完成上述操作后,数据库 tb_user 表和 tb_orders 表中的数据如图所示。
( 2 )在 com.neuedu.po 包中,创建持久化类 Orders 和 User,并在两个类中定义相关属性和方法,文件如下所示。
package com.neuedu.po; /** * 订单持久化类 */ public class Orders { private Integer id; //订单id private String number; //订单编号 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } @Override public String toString() { return "Orders [id=" + id + ", number=" + number + "]"; } }
package com.neuedu.po; import java.util.List; /** * 用户持久化类 */ public class User { private Integer id; //用户编号 private String username; //用户姓名 private String address; //用户地址 private List
ordersList; //用户关联的订单 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List getOrdersList() { return ordersList; } public void setOrdersList(List ordersList) { this.ordersList = ordersList; } @Override public String toString() { return "User [id=" + id + ", username=" + username + ", address=" + address + ", ordersList=" + ordersList + "]"; } } 在上述两个文件中,分别定义了各自的属性以及对应的 getter/setter 方法,同时为了方便查看输出结果,还重写了 toString() 方法。
( 3 )在 com.neuedu.mapper 包中,创建用户实体映射文件 UserMapper.xml ,并在文件中编写一对多关联映射查询的配置,文件如下所示。
在文件中,使用了 MyBatis 嵌套结果的方式定义了一个根据用户 id 查询用户及其关联的订单信息的 select 语句。 因为返回的用户对象中,包含 Orders 集合对象属性,所以需要手动编写结果映射信息。
( 4 )将映射文件 UserMapper.xml 的路径配置到核心配置文件 mybatis-config.xml 中,其代码如下所示。
( 5 )在测试类 MybatisAssociatedTest 中,编写测试方法 findUserTest() ,其代码如下所示。
/** * 一对多 */ @Test public void findUserTest(){ // 1.通过工具类生成SqlSession对象 SqlSession sqlSession = MybatisUtils.getSession(); // 2.查询 id 为 1 的用户信息、 User user = sqlSession.selectOne("com.neuedu.mapper.UserMapper.findUserWithOrders", 1); // 3.输出查询结果信息 System.out.println(user); // 4.关闭SQlSession sqlSession.close(); }
使用 JUnit4 执行 findUserTest() 方法后,控制台输出结果如图所示。
从图中可以看出,使用 MyBatis 嵌套结果的方式查询出了用户及其关联的订单集合信息。 这就是 MyBatis 一对多的关联查询。
需要注意的是 ,上述案例从用户的角度出发,用户与订单之间是一对多的关联关系,但如果从单个订单的角度出发,一个订单只能属于一个用户,即一对一的关联关系。 大家可根据前面小节内容实现单个订单与用户之间的一对一关联关系,由于篇幅有限,这里不再重复赘述。
多对多
在实际项目开发中,多对多的关联关系也是非常常见的。 以订单和商品为例,一个订单可以包含多种商品,而一种商品又可以属于多个订单,订单和商品就属于多对多的关联关系, 如图所示。
在数据库中,多对多的关联关系通常使用一个中间表来维护,中间表中的订单 id 作为外键参照订单表的 id ,商品 id 作为外键参照商品表的 id。 这三个表之间的关系如图所示。
了解了数据库中订单表与商品表之间的多对多关联关系后,下面我们就通过具体的案例来讲解如何使用 MyBatis 来处理这种多对多的关系,具体实现步骤如下。
( 1 )创建数据表。 在 mybatis 数据库中新建名称为 tb_product 和 tb_ordersitem 的两个数据表,同时在表中预先插入几条数据。 由于订单表在前面小节中已经创建 , 所以这里只创建了商品表和中间表。 完成上述操作后, tb_product 表和 tb_ordersitem 表中的数据如图所示。
( 2 ) 在 com.neuedu.po 包中,创建持久化类 Product, 并在类中定义相关属性和方法,文件如下所示。
package com.neuedu.po; import java.util.List; /** * 商品持久化类 */ public class Product { private Integer id; //商品 id private String name; //商品名称 private Double price; //商品单价 private List
orders; //与订单关联的属性 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public List getOrders() { return orders; } public void setOrders(List orders) { this.orders = orders; } @Override public String toString() { return "Product [id=" + id + ", name=" + name + ", price=" + price + ", orders=" + orders + "]"; } } 除了在商品持久化类中需要添加订单的集合属性外,还需要在订单持久化类( Orders.java ) 中增加商品集合的属性及其对应的 getter/setter 方法,同时为了方便查看输出结果,需要重写 toString() 方法, Orders 类中添加的代码如下所示。
private List
productList; //关联商品集合 //省略getter和setter方法 //省略toString()方法 ( 3 ) 在 com.neuedu.mapper 包中,创建订单实体映射文件 OrdersMapper.xml 和商品实体映射文件 ProductMapper.xml ,对两个映射文件进行编辑后,文件如下所示。
select="com.neuedu.mapper.ProductMapper.findProductById">
在文件中,使用嵌套查询的方式定义了一个 id 为 findOrdersWithPorduct 的 select 语句来查询订单及其关联的商品信息。 在
元素中使用了 元素来映射多对多的关联关系,其中 property 属性表示订单持久化类中的商品属性, ofType 属性表示集合中的数据为 Product 类型,而 column 的属性值会作为参数执行 ProductMapper 中定义的 id 为 findProductByld 的执行语句来查询订单中的商品信息。
在文件中,定义了一个 id 为 findProductByld 的执行语句, 该执行语句中的 SQL 会根据订单 id 查询与该订单所关联的商品信息。 由于订单和商品是多对多的关联关系,所以需要通过中间表来查询商品信息。
( 4 )将新创建的映射文件 OrdersMapper.xml 和 ProductMapper.xml 的文件路径配置到核心配置文件 mybatis-config.xml 中 ,代码如下所示。
( 5 )在测试类 MybatisAssociatedTest 中,编写多对多关联查询的测试方法 findOrdersTest() , 其代码如下所示。
/** * 多对多 */ @Test public void findOrdersTest(){ // 1.通过工具类生成SqlSession对象 SqlSession sqlSession = MybatisUtils.getSession(); // 2.查询 id 为 1 的用户信息、 Orders orders = sqlSession.selectOne("com.neuedu.mapper.OrdersMapper.findOrdersWithProduct", 1); // 3.输出查询结果信息 System.out.println(orders); // 4.关闭SQlSession sqlSession.close(); }
使用 JUnit4 执行 findOrdersTest() 方法后,控制台的输出结果如图所示。
从图中可以看出,使用 MyBatis 嵌套查询的方式执行了两条 SOL 语句,并查询出了订单及其关联的商品信息,这就是 MyBatis 多对多的关联查询。
如果大家对多表关联查询的 SOL 语句比较熟,也可以在 OrdersMapper.xml 中使用嵌套结果的方式,其代码如下所示。
在上述执行代码中,只定义了一条查询 SOL ,通过该 SOL 即可查询出订单及其关联的商品信息。
本章小结
本章首先对开发中涉及的数据表之间以及对象之间的关联关系做了简要介绍,并由此引出了 MyBatis 框架中对关联关系的处理;然后通过案例对 MyBatis 框架处理实体对象之间的三种关联关系进行了详细讲解。通过本章的学习,大家可以了解数据表以及对象中所涉及的三种关联关系,并能够使用 MyBatis 框架对三种关联关系的查询进行处理。 MyBatis 中的关联查询操作在实际开发中非常普遍,熟练掌握这三种关联查询方式有助于提高项目的开发效率,因此大家一定要多加练习。