10.1.3 多对多 在实际项目开发中,多对多关系也是非常常见的关系,比如,一个购物系统中,一个用户可以有多个订单,这是一对多的关系;一个订单中可以购买多种商品,一种商品也可以属于多个不同的订单,订单和商品之间就是多对多的关系 。 对于数据库中多对多的关系建议使用一个中间表来维护关系 ,中间表中的订单id
作为外键参照订单表的id
,商品id
作为外键参照商品表的id
。 下面我们就用一个示例来看看MyBatis
怎么处理多对多关系。
示例: ManyToManyTest 项目结构 展开/折叠
D:\Desktop\书籍\Java\Java EE\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\10\ManyToManyTest
├─src\
│ ├─db.properties
│ ├─log4j.xml
│ ├─mybatis-config.xml
│ └─org\
│ └─fkit\
│ ├─domain\
│ │ ├─Article.java
│ │ ├─Order.java
│ │ └─User.java
│ ├─factory\
│ ├─mapper\
│ │ ├─ArticleMapper.xml
│ │ ├─OrderMapper.java
│ │ ├─OrderMapper.xml
│ │ ├─UserMapper.java
│ │ └─UserMapper.xml
│ └─test\
│ └─ManyToManyTest.java
└─WebContent\
├─META-INF\
│ └─MANIFEST.MF
└─WEB-INF\
├─lib\
│ ├─ant-1.9.6.jar
│ ├─ant-launcher-1.9.6.jar
│ ├─asm-5.2.jar
│ ├─cglib-3.2.5.jar
│ ├─commons-logging-1.2.jar
│ ├─javassist-3.22.0-CR2.jar
│ ├─log4j-1.2.17.jar
│ ├─log4j-api-2.3.jar
│ ├─log4j-core-2.3.jar
│ ├─mybatis-3.4.5.jar
│ ├─mysql-connector-java-5.1.44-bin.jar
│ ├─ognl-3.1.15.jar
│ ├─slf4j-api-1.7.25.jar
│ └─slf4j-log4j12-1.7.25.jar
└─web.xml
创建数据库表 首先,给之前创建的mybatis
数据库创建三个表tb_user
、tb_article
和tb_order
,再创建一个中间表tb_item
维护tb_article
和tb_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 59 60 61 62 63 64 65 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_article
和tb_order
的多对多关系,
tb_item
表的
order_id
作为外键参照tb_order
表的主键id
,
article_id
作为外键参照tb_artic1e
表的主键id
。
在mybatis
数据库中执行SQL
脚本,完成创建数据库和表的操作。
创建持久化类 接下来,创建一个User
对象、一个Article
对象和一个Order
对象分别映射tb_user
,tb_article
和tb_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; private String username; private String loginname; private String password; private String phone; private String address; private List<Order> orders; public User () { super (); } 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; } @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; private String code; private Double total; private User user; private List<Article> articles; public Order () { super (); } public Order (String code, Double total) { super (); this .code = code; this .total = total; } @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; private String name; private Double price; private String remark; private List<Order> orders; public Article () { super (); } public Article (String name, Double price, String remark) { super (); this .name = name; this .price = price; this .remark = remark; } @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" ><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 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
类除了简单的属性id
、username
、loginname
、password
、phone
和address
之外,还有关联对象orders
,所以返回的是一个名为userResultMap
的resultMap
。由于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" ><mapper namespace ="org.fkit.mapper.OrderMapper" > <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 > <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 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 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 >
selectOrderByUserId OrderMapper.xml
中定义了一个id="selectOrderByUserId"
的select标签.其根据用户id
查询订单信息,返回的是简单的Order
对象。
1 2 3 4 5 6 <select id ="selectOrderByUserId" parameterType ="int" resultType ="org.fkit.domain.Order" > SELECT * FROM tb_order WHERE user_id = #{id}</select >
selectOrderById 同时定义了一个id="selectOrderById"
的select标签,其根据订单id查询订单信息,由于Order
类和用户是多对一关系,和商品是多对多关系,而多对一通常都是立即加载 ,因此SQL语句是一条关联了tb_user
和tb_order
的多表查询语句。查询结果返回个名为orderResultMap
的resultMap
。
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" ><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"
的select标签,其根据订单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 = null ; try { sqlSession = FKSqlSessionFactory.getSqlSession(); ManyToManyTest t = new ManyToManyTest (); t.testSelectOrderById(sqlSession); sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); e.printStackTrace(); } finally { if (sqlSession != null ) sqlSession.close(); } } public void testSelectUserById (SqlSession sqlSession) { UserMapper um = sqlSession.getMapper(UserMapper.class); User user = um.selectUserById(1 ); System.out.println(user.getId() + " " + user.getUsername()); List<Order> orders = user.getOrders(); orders.forEach(order -> System.out.println(order)); } public void testSelectOrderById (SqlSession sqlSession) { OrderMapper om = sqlSession.getMapper(OrderMapper.class); Order order = om.selectOrderById(2 ); System.out.println(order.getId() + " " + order.getCode() + " " + order.getTotal()); User user = order.getUser(); System.out.println(user); } }
testSelectUserById 运行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 接下来测试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标签的属性
column
属性指定要将哪一列,作为select
标签关联的查询语句的参数。
select
属性指定关联查询的select
标签
property
属性指定接收select结果集的集合的变量名
javaType
属性指定集合的类型
ofType
属性指定集合中存放的元素的类型
collection标签的子标签 这些子标签表示集合中元素的各个属性。
association标签 对于多对一,一对一联系,使用多表连接查询
即可。
association标签的属性
property
设置持久化对象的属性
javaType
设置该属性的类型
association标签的子标签 这些子标签表示关联的持久化对象的各个属性。