0%

10.1.3 多对多

10.1.3 多对多

在实际项目开发中,多对多关系也是非常常见的关系,比如,一个购物系统中,一个用户可以有多个订单,这是一对多的关系;一个订单中可以购买多种商品,一种商品也可以属于多个不同的订单,订单和商品之间就是多对多的关系。对于数据库中多对多的关系建议使用一个中间表来维护关系,中间表中的订单id作为外键参照订单表的id,商品id作为外键参照商品表的id下面我们就用一个示例来看看MyBatis怎么处理多对多关系.

示例: ManyToManyTest

创建数据库表

首先,给之前创建的mybatis数据库创建三个表tb_usertb_articletb_order,再创建一个中间表维护tb_articletb_order的关系,并插入测试数据。SQL脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
use mybatis;
# 创建用户表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(18) DEFAULT NULL,
`loginname` varchar(18) NOT NULL,
`password` varchar(18) NOT NULL,
`phone` varchar(18) DEFAULT NULL,
`address` varchar(18) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入用户
INSERT INTO `tb_user` VALUES ('1', '小明', 'xiaoming', 'xiaoming', '123456789123', '北京');
# 创建商品表
DROP TABLE IF EXISTS `tb_article`;
CREATE TABLE `tb_article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`price` double DEFAULT NULL,
`remark` varchar(18) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入商品
INSERT INTO `tb_article` VALUES ('1', '商品1', '123.12', 'xxx的伟大著作');
INSERT INTO `tb_article` VALUES ('2', '商品2', '12.3', 'yyy的伟大著作');
INSERT INTO `tb_article` VALUES ('3', '商品3', '34.22', 'zzz的著作');
# 创建订单表
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(32) DEFAULT NULL,
`total` double DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `tb_order_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入订单
INSERT INTO `tb_order` VALUES ('1', 'abcseeeahoaugoeijgiej', '223.33', '1');
INSERT INTO `tb_order` VALUES ('2', 'sfaofosfhodsfuefie', '111.22', '1');
# 创建订单-商品关系表
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
`order_id` int(11) NOT NULL DEFAULT '0',
`article_id` int(11) NOT NULL DEFAULT '0',
`amount` int(11) DEFAULT NULL,
PRIMARY KEY (`order_id`,`article_id`),
KEY `article_id` (`article_id`),
CONSTRAINT `tb_item_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `tb_order` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tb_item_ibfk_2` FOREIGN KEY (`article_id`) REFERENCES `tb_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入商品-订单
INSERT INTO `tb_item` VALUES ('1', '1', '1');
INSERT INTO `tb_item` VALUES ('1', '2', '1');
INSERT INTO `tb_item` VALUES ('1', '3', '3');
INSERT INTO `tb_item` VALUES ('2', '1', '2');
INSERT INTO `tb_item` VALUES ('2', '2', '3');
  • tb_order表的user_id作为外键参照tb_user表的主键id
  • tb_item表作为中间表,用来维护tb_articletb_order的多对多关系,tb_item表的
    • order_id作为外键参照tb_order表的主键id,
    • article_id作为外键参照tbartic1e表的主键id.

mybatis数据库中执行SQL脚本,完成创建数据库和表的操作。

创建持久化类

接下来,创建一个User对象、一个Article对象和一个Order对象分别映射tb_user,tb_articletb_order表。

创建持久化类User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 用户id,主键
private String username; // 用户名
private String loginname; // 登录名
private String password; // 密码
private String phone; // 联系电话
private String address; // 收货地址
// 用户和订单是一对多的关系,即一个用户可以有多个订单
private List<Order> orders;
public User()
{
super();
// TODO Auto-generated constructor stub
}
public User(String username, String loginname, String password, String phone, String address)
{
super();
this.username = username;
this.loginname = loginname;
this.password = password;
this.phone = phone;
this.address = address;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", username=" + username + ", loginname=" + loginname
+ ", password=" + password + ", phone=" + phone + ", address=" + address + "]";
}
}

用户和订单之间是一对多的关系,即一个用户可以有多个订单。在User类中定义了个orders属性,该属性是一个List集合,用来映射一对多的关联关系,表示一个用户有多个订单.

创建持久化类Order.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Order
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 订单id,主键
private String code; // 订单编号
private Double total; // 订单总金额
// 订单和用户是多对一的关系,即一个订单只属于一个用户
private User user;
// 订单和商品是多对多的关系,即一个订单可以包含多种商品
private List<Article> articles;
public Order()
{
super();
// TODO Auto-generated constructor stub
}
public Order(String code, Double total)
{
super();
this.code = code;
this.total = total;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Order [id=" + id + ", code=" + code + ", total=" + total + "]";
}
}
  • 订单和用户之间是多对一的关系,一个订单只属于一个用户,在Order类中定义了一个user属性,用来映射多对一的关联关系,表示该订单的用户;
  • 订单和商品之间是多对多的关系,即一个订单中可以包含多种商品,在Order类中定义了一个articles属性该属性是一个List集合,用来映射多对多的关联关系,表示一个订单中包含多种商品。

创建持久化类Article.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Article
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 商品id,主键
private String name; // 商品名称
private Double price; // 商品价格
private String remark; // 商品描述
// 商品和订单是多对多的关系,即一种商品可以包含在多个订单中
private List<Order> orders;
public Article()
{
super();
// TODO Auto-generated constructor stub
}
public Article(String name, Double price, String remark)
{
super();
this.name = name;
this.price = price;
this.remark = remark;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Article [id=" + id + ", name=" + name + ", price=" + price + ", remark=" + remark
+ "]";
}
}

商品和订单之间是多对多的关系,即一种商品可以出现在多个订单中。在Article类中定义了一个orders属性,该属性是一个List集合,用来映射多对多的关联关系,表示该商品关联的多个订单。

创建XML映射文件

再接下来是XML映射文件。

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.UserMapper">
<select
id="selectUserById"
parameterType="int"
resultMap="userResultMap"> SELECT * FROM tb_user WHERE id = #{id}
</select>
<resultMap
type="org.fkit.domain.User"
id="userResultMap">
<id
property="id"
column="id"/>
<result
property="username"
column="username"/>
<result
property="loginname"
column="loginname"/>
<result
property="password"
column="password"/>
<result
property="phone"
column="phone"/>
<result
property="address"
column="address"/>
<!-- 一对多关联映射:collection -->
<collection
property="orders"
javaType="ArrayList"
column="id"
ofType="org.fkit.domain.User"
select="org.fkit.mapper.OrderMapper.selectOrderByUserId"
fetchType="lazy">
<id
property="id"
column="id"/>
<result
property="code"
column="code"/>
<result
property="total"
column="total"/>
</collection>
</resultMap>
</mapper>

UserMapper.xml中定义了一个select标签,该标签根据id查询用户信息。由于User类除了简单的属性idusernamelginnanepasswordphoneaddress之外,还有关联对象orders,所以返回的是一个名为userResultMapresultMap。由于orders是一个List集合,因此userResultMap中使用了collection元素映射一对多的关联关系。
collection元素说明如下:

  • select属性表示会使用column属性的id值作为参数执行OrderMapper中定义的selectOrderByUserId标签,以查询该用户下的所有订单,
  • 查询出的数据将被封装到property表示的orders对象当中。注意,一对多使用的都是lazy(懒加载)。

OrderMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.OrderMapper">
<!-- 注意,如果查询出来的列同名,例如tb_user表的id和tb_order表的id都是id,同名,需要使用别名区分 -->
<select
id="selectOrderById"
parameterType="int"
resultMap="orderResultMap"> SELECT u.*,o.id AS oid,CODE,total,user_id FROM tb_user
u,tb_order o WHERE u.id = o.user_id AND o.id = #{id}
</select>
<!-- 根据userid查询订单 -->
<select
id="selectOrderByUserId"
parameterType="int"
resultType="org.fkit.domain.Order"> SELECT * FROM tb_order WHERE user_id = #{id}
</select>
<resultMap
type="org.fkit.domain.Order"
id="orderResultMap">
<id
property="id"
column="oid"/>
<result
property="code"
column="code"/>
<result
property="total"
column="total"/>
<!-- 多对一关联映射:association -->
<association
property="user"
javaType="org.fkit.domain.User">
<id
property="id"
column="id"/>
<result
property="username"
column="username"/>
<result
property="loginname"
column="loginname"/>
<result
property="password"
column="password"/>
<result
property="phone"
column="phone"/>
<result
property="address"
column="address"/>
</association>
<!-- 多对多映射的关键:collection -->
<collection
property="articles"
javaType="ArrayList"
column="oid"
ofType="org.fkit.domain.Article"
select="org.fkit.mapper.ArticleMapper.selectArticleByOrderId"
fetchType="lazy">
<id
property="id"
column="id"/>
<result
property="name"
column="name"/>
<result
property="price"
column="price"/>
<result
property="remark"
column="remark"/>
</collection>
</resultMap>
</mapper>

OrderMapper.xml中定义了一个id="selectOrderByUserld"的select标签.其根据用户id查询订单信息,返回的是简单的Order对象.

1
2
3
4
5
6
<!-- 根据userid查询订单 -->
<select
id="selectOrderByUserId"
parameterType="int"
resultType="org.fkit.domain.Order"> SELECT * FROM tb_order WHERE user_id = #{id}
</select>

同时定义了一个id="selectOrderById"的select标签,其根据订单id查询订单信息,由于Order类和用户是多对一关系,和商品是多对多关系,而多对一通常都是立即加载,因此SQL语句是一条关联了tb_usertb_order的多表查询语句。查询结果返回个名为orderResultMapresultMap

orderResultMap中使用了association元素映射多对一的关联关系,其将查询到的用户信息装载到Order对象的user属性当中.

1
2
3
4
5
6
<select
id="selectOrderById"
parameterType="int"
resultMap="orderResultMap"> SELECT u.*,o.id AS oid,CODE,total,user_id FROM tb_user
u,tb_order o WHERE u.id = o.user_id AND o.id = #{id}
</select>

orderResultMap中还使用了collection元素映射多对多的关联关系,select属性表示会使用column属性的oid值作为参数执行ArticleMapper中定义的selectArticleByOrderId查询该订单中的所有商品,查询出的数据将被封装到property属性表示的articles对象当中。注意,一对多使用的都是lazy(懒加载).

提示

因为多表查询返回的结果集中tb_user有个id列,tb_order也有个id列,当列同名时MyBatis使用的元素中的colum属性如果是id,则MyBatis会默认使用査询出的第一个id列。为了区分同名的列,最好的方法是给列取一个别名SQL语句中的o.id As oid,resultMap中的column="oid"就是指使用的是tb_order表的id值。

ArticleMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.ArticleMapper">
<select
id="selectArticleByOrderId"
parameterType="int"
resultType="org.fkit.domain.Article"> SELECT * FROM tb_article WHERE id IN ( SELECT article_id
FROM tb_item WHERE order_id = #{id} )
</select>
</mapper>

ArticleMapper.xml中定义了一个id="selectArticleByOrderId"的selet标签,其根据订单id查询订单关联的所有商品,由于订单和商品是多对多的关系,数据库使用了一个中间表tb_item维护多对多的关系,故此处使用了一个子查询,首先根据订单id到中间表中査询出所有的商品,之后根据所有商品的id查询出所有的商品信息,并将这些信息封装到Article对象当中。

编写Mapper接口

再接下来是mapper接口对象。

UserMapper接口

1
2
3
public interface UserMapper {
User selectUserById(int id);
}

OrderMapper接口

1
2
3
public interface OrderMapper {
Order selectOrderById(int id);
}

编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ManyToManyTest {
public static void main(String[] args)
{
// 定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
ManyToManyTest t = new ManyToManyTest();
// 根据用户id查询用户,测试一对多
// t.testSelectUserById(sqlSession);
// 根据订单id查询订单,测试多对多
t.testSelectOrderById(sqlSession);
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}
// 测试一对多,查询班级User(一)的时候级联查询订单Order(多)
public void testSelectUserById(SqlSession sqlSession)
{
// 获得UserMapper接口的代理对象
UserMapper um = sqlSession.getMapper(UserMapper.class);
// 调用selectUserById方法
User user = um.selectUserById(1);
// 查看查询到的user对象信息
System.out.println(user.getId() + " " + user.getUsername());
// 查看user对象关联的订单信息
List<Order> orders = user.getOrders();
orders.forEach(order -> System.out.println(order));
}
// 测试多对多,查询订单Order(多)的时候级联查询订单的商品Article(多)
public void testSelectOrderById(SqlSession sqlSession)
{
// 获得OrderMapper接口的代理对象
OrderMapper om = sqlSession.getMapper(OrderMapper.class);
// 调用selectOrderById方法
Order order = om.selectOrderById(2);
// 查看查询到的order对象信息
System.out.println(order.getId() + " " + order.getCode() + " " + order.getTotal());
// 查看order对象关联的用户信息
User user = order.getUser();
System.out.println(user);
// // 查看order对象关联的商品信息
// List<Article> articles = order.getArticles();
// articles.forEach(article -> System.out.println(article));
}
}

运行ManyToManyTest类的main方法,首先测试testSelectUserById()方法,根据用户id查询用户。控制台显示如下:

1
2
3
4
5
6
7
8
9
DEBUG [main] ==>  Preparing: SELECT * FROM tb_user WHERE id = ? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
1 小明
DEBUG [main] ==> Preparing: SELECT * FROM tb_order WHERE user_id = ?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 2
Order [id=1, code=abcseeeahoaugoeijgiej, total=223.33]
Order [id=2, code=sfaofosfhodsfuefie, total=111.22]

可以看到,MyBatis执行了根据用户id查询用户的SQL语句,查询出了用户信息;
由于在测试方法中立即又获取了用户的订单集合,所以MyBatis又执行了根据用户id查询订单的SQL语句,查询出了该用户的两个订单.
接下来测试testSelectOrderById()方法,根据订单id查询订单信息。控制台显示:

1
2
3
4
5
DEBUG [main] ==>  Preparing: SELECT u.*,o.id AS oid,CODE,total,user_id FROM tb_user u,tb_order o WHERE u.id = o.user_id AND o.id = ? 
DEBUG [main] ==> Parameters: 2(Integer)
DEBUG [main] <== Total: 1
2 sfaofosfhodsfuefie 111.22
User [id=1, username=小明, loginname=xiaoming, password=xiaoming, phone=123456789123, address=北京]

可以看到,MyBatis执行了一个多表连接查询,同时查询出了订单信息和用户信息,由于测试方法中注释了查询订单中的商品代码,故MyBatis采用了懒加载机制,没有立即查询商品信息。
取消testSelectOrderById()方法中査询订单中的商品的代码注释,再次执行。控制台显示如下:

1
2
3
4
5
6
7
8
9
10
DEBUG [main] ==>  Preparing: SELECT u.*,o.id AS oid,CODE,total,user_id FROM tb_user u,tb_order o WHERE u.id = o.user_id AND o.id = ? 
DEBUG [main] ==> Parameters: 2(Integer)
DEBUG [main] <== Total: 1
2 sfaofosfhodsfuefie 111.22
User [id=1, username=小明, loginname=xiaoming, password=xiaoming, phone=123456789123, address=北京]
DEBUG [main] ==> Preparing: SELECT * FROM tb_article WHERE id IN ( SELECT article_id FROM tb_item WHERE order_id = ? )
DEBUG [main] ==> Parameters: 2(Integer)
DEBUG [main] <== Total: 2
Article [id=1, name=商品1, price=123.12, remark=xxx的伟大著作]
Article [id=2, name=商品2, price=12.3, remark=yyy的伟大著作]

可以看到,MyBatis执行了ArticleMapper.xml中定义的子查询,查询出了订单所关联的所有商品信息.

总结

collection标签

collection标签的属性

-property属性指定集合的变量名
-javaType属性指定集合的类型
-ofType属性指定集合中存放的元素的类型
-select属性指定关联查询的select标签
-column属性指定要将哪一列,作为select标签关联的查询语句的参数.

collection标签的子标签

这些子标签表示集合中元素的各个属性.

association标签

对于多对一,一对一联系,使用多表连接查询即可.

association标签的属性

  • property设置持久化对象的属性
  • javaType设置该属性的类型

association标签的子标签

这些子标签表示关联的持久化对象的各个属性.

原文链接: 10.1.3 多对多