请选择 进入手机版 | 继续访问电脑版
本站特色:极好的技术研究氛围!所有技术发帖,必有回复! 做最受欢迎的Java论坛

疯狂Java联盟

 找回密码
 加入联盟
查看: 31|回复: 0

MyBatis连载(10)-- MyBatis关联映射之多对多

[复制链接]
发表于 2017-12-6 20:41:43 | 显示全部楼层 |阅读模式
本帖最后由 xiaowenji 于 2017-12-7 20:14 编辑

本文节选自《Spring+MyBatis企业应用实战》

京东购买地址:https://item.jd.com/12111732.html
本书完整代码下载地址:http://www.crazyit.org/thread-12008-1-1.html
本小节完整代码:codes/10/ManyToManyTest

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

示例:ManyToManyTest

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

  1. #创建用户表
  2. CREATE TABLE tb_user(
  3. id INT PRIMARY KEY AUTO_INCREMENT,
  4. username VARCHAR(18),
  5. loginname VARCHAR(18),
  6. PASSWORD VARCHAR(18),
  7. phone VARCHAR(18),
  8. address VARCHAR(18)
  9. );
  10. #插入用户表测试数据
  11. INSERT INTO tb_user(username,loginname,PASSWORD,phone,address)
  12. VALUES('杰克','jack','123456','13920001616','广州');
  13. #创建商品表
  14. CREATE TABLE tb_article(
  15. id INT PRIMARY KEY AUTO_INCREMENT,
  16. NAME VARCHAR(18),
  17. price DOUBLE,
  18. remark VARCHAR(18)
  19. );
  20. #插入商品表测试数据
  21. INSERT INTO tb_article(NAME,price,remark)
  22. VALUES('疯狂Java讲义',108.9,'李刚老师经典著作');
  23. INSERT INTO tb_article(NAME,price,remark)
  24. VALUES('疯狂Android讲义',99.9,'李刚老师经典著作');
  25. INSERT INTO tb_article(NAME,price,remark)
  26. VALUES('疯狂iOS讲义',89.9,'李刚老师经典著作');
  27. INSERT INTO tb_article(NAME,price,remark)
  28. VALUES('SpringMVC+MyBatis企业开发',69.9,'肖文吉老师经典著作');
  29. #创建订单表
  30. CREATE TABLE tb_order(
  31. id INT PRIMARY KEY AUTO_INCREMENT,
  32. CODE VARCHAR(32),
  33. total DOUBLE,
  34. user_id INT,
  35. FOREIGN KEY (user_id) REFERENCES tb_user(id)
  36. );
  37. #插入订单表测试数据
  38. INSERT INTO tb_order(CODE,total,user_id)
  39. VALUES('6aa3fa359ff14619b77fab5990940a2d',388.6,1);
  40. INSERT INTO tb_order(CODE,total,user_id)
  41. VALUES('6aa3fa359ff14619b77fab5990940b3c',217.8,1);
  42. #创建中间表
  43. CREATE TABLE tb_item(
  44. order_id INT,
  45. article_id INT,
  46. amount INT,
  47. PRIMARY KEY(order_id,article_id),
  48. FOREIGN KEY (order_id) REFERENCES tb_order(id),
  49. FOREIGN KEY (article_id) REFERENCES tb_article(id)
  50. );
  51. #创建插入中间表数据
  52. INSERT INTO tb_item(order_id,article_id,amount)
  53. VALUES(1,1,1);
  54. INSERT INTO tb_item(order_id,article_id,amount)
  55. VALUES(1,2,1);
  56. INSERT INTO tb_item(order_id,article_id,amount)
  57. VALUES(1,3,2);
  58. INSERT INTO tb_item(order_id,article_id,amount)
  59. VALUES(2,4,2);
  60. INSERT INTO tb_item(order_id,article_id,amount)
  61. VALUES(2,1,1);
复制代码

tb_order表的user_id作为外键参照tb_user表的主键id。tb_item表作为中间表,用来维护tb_article和tb_order的多对多关系,tb_imte表的order_id作为外键参照tb_order表的主键id,article_id作为外键参照tb_article表的主键id。

在mybatis数据库中执行SQL脚本,完成创建数据库和表的操作。
接下来,创建一个User对象、一个Article对象和一个Order对象分别映射tb_user、tb_article和tb_order表。

程序清单:codes/10/ManyToManyTest/src/org/fkit/domain/User.java
  1. public class User implements Serializable{
  2.     private Integer id;  // 用户id,主键
  3.     private String username;  // 用户名
  4.     private String loginname; // 登录名
  5.     private String password;  // 密码
  6.     private String phone;    // 联系电话
  7.     private String address;  // 收货地址
  8.     // 用户和订单是一对多的关系,即一个用户可以有多个订单
  9.     private List<Order> orders;
  10.     // 省略构造器和set/get方法……
  11. }
复制代码

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

程序清单:codes/10/ManyToManyTest/src/org/fkit/domain/Order.java
  1. public class Order implements Serializable {
  2.     private Integer id;  // 订单id,主键
  3.     private String code;  // 订单编号
  4.     private Double total; // 订单总金额
  5.     // 订单和用户是多对一的关系,即一个订单只属于一个用户
  6.     private User user;
  7.     // 订单和商品是多对多的关系,即一个订单可以包含多种商品
  8.     private List<Article> articles;
  9.     // 省略构造器和set/get方法……
  10. }
复制代码

订单和用户是多对一的关系,一个订单只属于一个用户,在Order类中定义了一个user属性,用来映射多对一的关联关系,表示该订单的用户;订单和商品是多对多的关系,即一个订单中可以包含多种商品,在Order类中定义了一个articles属性,该属性是一个List集合,用来映射多对多的关联关系,表示一个订单中包含多种商品。

程序清单:codes/10/ManyToManyTest/src/org/fkit/domain/Articlejava
  1. public class Article implements Serializable {
  2.     private Integer id;        // 商品id,主键
  3.     private String name;    // 商品名称
  4.     private Double price;    // 商品价格
  5.     private String remark;    // 商品描述
  6.     // 商品和订单是多对多的关系,即一种商品可以出现在多个订单中
  7.     private List<Order> orders;
  8.     // 省略构造器和set/get方法......
  9. }
复制代码
商品和订单是多对多的关系,即一种商品可以出现在多个订单中。在Article类中定义了一个orders属性,该属性是一个List集合,用来映射多对多的关联关系,表示该商品关联的多个订单。
再接下来是XML映射文件。

程序清单:codes/10/ManyToManyTest/src/org/fkit/mapper/UserMapper.xml
  1. <mapper namespace="org.fkit.mapper.UserMapper">
  2.     <resultMap type="org.fkit.domain.User" id="userResultMap">
  3.         <id property="id" column="id"/>
  4.         <result property="username" column="username"/>
  5.         <result property="loginname" column="loginname"/>
  6.         <result property="password" column="password"/>
  7.         <result property="phone" column="phone"/>
  8.         <result property="address" column="address"/>
  9.         <!-- 一对多关联映射:collection   -->
  10.         <collection property="orders" javaType="ArrayList"
  11.       column="id" ofType="org.fkit.domain.User"
  12.       select="org.fkit.mapper.OrderMapper.selectOrderByUserId"
  13.       fetchType="lazy">
  14.           <id property="id" column="id"/>
  15.           <result property="code" column="code"/>
  16.           <result property="total" column="total"/>
  17.       </collection>
  18.     </resultMap>
  19.   <select id="selectUserById" parameterType="int" resultMap="userResultMap">
  20.       SELECT * FROM tb_user  WHERE id = #{id}
  21.   </select>
  22. </mapper>
复制代码

UserMapper.xml中定义了一个<select.../>,其根据id查询用户信息。由于User类除了简单的属性id、username、loginname、password、phone和address之外,还有一个关联对象orders,所以返回的是一个名为userResultMap的resultMap。由于orders是一个List集合,因此userResultMap中使用了<collection.../>元素映射一对多的关联关系,select属性表示会使用column属性的id值作为参数执行OrderMapper中定义的selectOrderByUserId查询该用户所下的所有订单,查询出的数据将被封装到property表示的orders对象当中。注意,一对多使用的都是lazy(懒加载)。

程序清单:codes/10/ManyToManyTest/src/org/fkit/mapper/OrderMapper.xml
  1. <mapper namespace="org.fkit.mapper.OrderMapper">
  2.     <resultMap type="org.fkit.domain.Order" id="orderResultMap">
  3.         <id property="id" column="oid"/>
  4.           <result property="code" column="code"/>
  5.           <result property="total" column="total"/>
  6.         <!-- 多对一关联映射:association   -->
  7.         <association property="user" javaType="org.fkit.domain.User">
  8.             <id property="id" column="id"/>
  9.             <result property="username" column="username"/>
  10.             <result property="loginname" column="loginname"/>
  11.             <result property="password" column="password"/>
  12.             <result property="phone" column="phone"/>
  13.             <result property="address" column="address"/>
  14.         </association>
  15.         <!-- 多对多映射的关键:collection   -->
  16.         <collection property="articles" javaType="ArrayList"
  17.       column="oid" ofType="org.fkit.domain.Article"
  18.       select="org.fkit.mapper.ArticleMapper.selectArticleByOrderId"
  19.       fetchType="lazy">
  20.           <id property="id" column="id"/>
  21.           <result property="name" column="name"/>
  22.           <result property="price" column="price"/>
  23.           <result property="remark" column="remark"/>
  24.       </collection>
  25.     </resultMap>
  26.     <!-- 注意,如果查询出来的列同名,例如tb_user表的id和tb_order表的id都是id,同名,则需要使用别名区分 -->
  27.   <select id="selectOrderById" parameterType="int" resultMap="orderResultMap">
  28.       SELECT u.*,o.id AS oid,CODE,total,user_id
  29.        FROM tb_user u,tb_order o
  30.       WHERE u.id = o.user_id
  31.        AND o.id = #{id}
  32.   </select>
  33.   <!-- 根据userid查询订单 -->
  34.   <select id="selectOrderByUserId" parameterType="int" resultType="org.fkit.domain.Order">
  35.       SELECT * FROM tb_order WHERE user_id = #{id}
  36.   </select>
  37. </mapper>
复制代码
OrderMapper.xml中定义了一个<select id=" selectOrderByUserId".../>,其根据用户id查询订单信息,返回的是简单的Order对象。
还定义了一<select id=" selectOrderById ".../>,其根据订单id查询订单信息,由于Order类和用户是多对一关系,和商品是多对多关系,而多对一通常都是立即加载,因此SQL语句是一条关联了tb_user和tb_order的多表查询语句。查询结果返回一个名为orderResultMap的resultMap。orderResultMap中使用了< association.../>元素映射多对一的关联关系,其将查询到的用户信息装载到Order对象的user属性当中;orderResultMap中还使用了<collection.../>元素映射多对多的关联关系,select属性表示会使用column属性的oid值作为参数执行ArticleMapper中定义的selectArticleByOrderId查询该订单中的所有商品,查询出的数据将被封装到property表示的articles对象当中。注意,一对多使用的都是lazy(懒加载)。

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

程序清单:codes/10/ManyToManyTest/src/org/fkit/mapper/ArticleMapper.xml
  1. <mapper namespace="org.fkit.mapper.ArticleMapper">
  2.   <select id="selectArticleByOrderId" parameterType="int"
  3.   resultType="org.fkit.domain.Article">
  4.       SELECT * FROM tb_article WHERE id IN (
  5.         SELECT article_id FROM tb_item WHERE order_id = #{id}
  6.     )
  7.   </select>
  8. </mapper>
复制代码

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

程序清单:codes/10/ManyToManyTest/src/org/fkit/mapper/UserMapper.java
  1. public interface UserMapper {
  2.     User selectUserById(int id);
  3. }
复制代码
程序清单:codes/10/ManyToManyTest/src/org/fkit/mapper/OrderMapper.java
  1. public interface OrderMapper {
  2.     Order selectOrderById(int id);
  3. }
复制代码
最后,完成测试类。

程序清单:codes/10/OneToManyTest/src/org/fkit/test/OneToManyTest.java
  1. public class ManyToManyTest {
  2.     public static void main(String[] args) throws Exception {
  3.         // 读取mybatis-config.xml文件
  4.         InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  5.         // 初始化mybatis,创建SqlSessionFactory类的实例
  6.         SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
  7.                 .build(inputStream);
  8.         // 创建Session实例
  9.         SqlSession session = sqlSessionFactory.openSession();
  10.         ManyToManyTest t = new ManyToManyTest();
  11.         // 根据用户id查询用户,测试一对多关系
  12.         t.testSelectUserById(session);
  13.         // 根据订单id查询订单,测试多对多关系
  14. //        t.testSelectOrderById(session);
  15.         // 提交事务
  16.         session.commit();
  17.         // 关闭Session
  18.         session.close();
  19.     }
  20.     // 测试一对多关系,查询班级User(一)的时候级联查询订单Order(多)  
  21.     public void testSelectUserById(SqlSession session){
  22.         // 获得UserMapper接口的代理对象
  23.         UserMapper um = session.getMapper(UserMapper.class);
  24.         // 调用selectUserById方法
  25.         User user = um.selectUserById(1);
  26.         // 查看查询到的user对象信息
  27.         System.out.println(user.getId() + " " + user.getUsername());
  28.         // 查看user对象关联的订单信息
  29.         List<Order> orders = user.getOrders();
  30.         for(Order order : orders){
  31.             System.out.println(order);
  32.         }
  33.     }
  34.     // 测试多对多关系,查询订单Order(多)的时候级联查询订单的商品Article(多)  
  35.     public void testSelectOrderById(SqlSession session){
  36.         // 获得OrderMapper接口的代理对象
  37.         OrderMapper om = session.getMapper(OrderMapper.class);
  38.         // 调用selectOrderById方法
  39.         Order order = om.selectOrderById(2);
  40.         // 查看查询到的order对象信息
  41.         System.out.println(order.getId() + " " + order.getCode() + " " + order.getTotal());
  42.         // 查看order对象关联的用户信息
  43.         User user = order.getUser();
  44.         System.out.println(user);
  45.         // 查看order对象关联的商品信息
  46.         /*List<Article> articles = order.getArticles();
  47.         for(Article article : articles){
  48.             System.out.println(article);
  49.         }*/
  50.     }
  51. }
复制代码

项目的mybatis-config.xml和log4j.properties文件内容请参考配套资源文件,此处不再赘述。
运行ManyToManyTest类的main方法,首先测试testSelectUserById()方法,根据用户id查询用户。控制台显示如下:

  1. DEBUG [main] - ==>  Preparing: SELECT * FROM tb_user WHERE id = ?
  2. DEBUG [main] - ==> Parameters: 1(Integer)
  3. DEBUG [main] - <==      Total: 1
  4. 1 杰克
  5. DEBUG [main] - ==>  Preparing: SELECT * FROM tb_order WHERE user_id = ?
  6. DEBUG [main] - ==> Parameters: 1(Integer)
  7. DEBUG [main] - <==      Total: 2
  8. Order [id=1, code=6aa3fa359ff14619b77fab5990940a2d, total=388.6]
  9. Order [id=2, code=6aa3fa359ff14619b77fab5990940b3c, total=217.8]
复制代码

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

  1. 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 = ?
  2. DEBUG [main] - ==> Parameters: 2(Integer)
  3. DEBUG [main] - <==      Total: 1
  4. 2 6aa3fa359ff14619b77fab5990940b3c 217.8
  5. User [id=1, username=杰克, loginname=jack, password=123456, phone=13920001616, address=广州]
复制代码

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

  1. 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 = ?
  2. DEBUG [main] - ==> Parameters: 2(Integer)
  3. DEBUG [main] - <==      Total: 1
  4. 2 6aa3fa359ff14619b77fab5990940b3c 217.8
  5. User [id=1, username=杰克, loginname=jack, password=123456, phone=13920001616, address=广州]
  6. DEBUG [main] - ==>  Preparing: SELECT * FROM tb_article WHERE id IN ( SELECT article_id FROM tb_item WHERE order_id = ? )
  7. DEBUG [main] - ==> Parameters: 2(Integer)
  8. DEBUG [main] - <==      Total: 2
  9. Article [id=1, name=疯狂Java讲义, price=108.9, remark=李刚老师经典著作]
  10. Article [id=4, name=SpringMVC+MyBatis企业开发, price=69.9, remark=肖文吉老师经典著作]
复制代码

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

多对多查询因为关联到中间表查询,所以读者需要对数据库的SQL知识有一定的了解。


您需要登录后才可以回帖 登录 | 加入联盟

本版积分规则

视频、代码、电子书下载
请关注"疯狂图书"公众号
QQ交流1群: 545923995  未满

小黑屋|手机版|Archiver|疯狂Java联盟 ( 粤ICP备11063141号 )

GMT+8, 2017-12-18 20:43 , Processed in 0.305102 second(s), 6 queries , File On.

快速回复 返回顶部 返回列表