分享

【mybatis】关联查询

 微雨渐落 2017-12-07

一、数据库

        演示的小demo涉及两张表,一张是Person表,记录用户基本信息,另一张是phone表,记录电话号码,Person的phoneID外键关联phone表id。此处示例涉及并不符合实际情况,一对多方式是一个号码有多个用户使用的(更符合实际情况的是一个用户有多个号码),原因是从一对一demo直接扒拉过来用的,见谅,不过也能体现mybatis关联查询一对多的细节了。

1. 数据库设计

  1. CREATE TABLE `tbl_person` (  
  2.    `id` int(11) NOT NULL AUTO_INCREMENT,  
  3.    `username` varchar(30) NOT NULL,  
  4.    `password` varchar(30) NOT NULL,  
  5.    `gender` varchar(30) DEFAULT NULL,  
  6.    `age` int(11) DEFAULT NULL,  
  7.    `phoneID` int(11) DEFAULT NULL,  
  8.    PRIMARY KEY (`id`),  
  9.    UNIQUE KEY `id` (`id`),  
  10.    UNIQUE KEY `username` (`username`),  
  11.    KEY `per_pho_fk` (`phoneID`),  
  12.    CONSTRAINT `per_pho_fk` FOREIGN KEY (`phoneID`) REFERENCES `tbl_phone` (`id`)  
  13.  ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8  
  1. CREATE TABLE `tbl_phone` (  
  2.    `id` int(11) NOT NULL AUTO_INCREMENT,  
  3.    `phoneNum` varchar(11) NOT NULL,  
  4.    PRIMARY KEY (`id`),  
  5.    UNIQUE KEY `phoneNum` (`phoneNum`)  
  6.  ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8  

2. 数据库补充

        phoneID作为tbl_person的外键,关联tbl_phone的主键id,此处假设的业务逻辑是一个号码存在多个用户。

        用例数据:

  1. id,username,password,gender,age,phoneID  
  2. 1,zhangsan,123,male,18,2  
  3. 2,lisi,123,female,24,2  
  4.   
  5. id,phoneNum  
  6. 2,15200898242  

二、一对多两种方式详解

        通过phone id查询电话信息,并关联查询拥有该电话的Person信息列表,一种方式是先查询tbl_phone表,获取到phone id,再通过phone id从tbl_person表中查询Person信息列表,这种方式要和数据库交互两次,发出两次sql语句,但是数据库返回的数据没有冗余;另一种方式是直接关联查询,只与数据库交互一次,只发出一次sql语句,但是数据库返回数据存在主表信息冗余,冗余数据为主表内容,冗余次数为从表中满足条件的记录数。

        因此,应该选择哪一种方式更合理,需要根据具体的业务场景来选择,没有绝对的正确,只有相对的适合。

        两种方式的实现,最核心的差异在于mapper.xml配置文件,完整的示例工程,需要mybatis-config.xml以及log4j的配置文件等,此处不一一列举出来,在第三部分附上完整代码download地址,此处仅仅关注核心问题——实现以及差异。两种方式的po类相同,主要关注Phone类中通过List<Person>体现关联关系,示例代码如下。

  1. package cn.wxy.domain;  
  2.   
  3. import java.io.Serializable;  
  4.   
  5. public class Person implements Serializable {  
  6.     private static final long serialVersionUID = 6028193740527306730L;  
  7.     private Integer id;  
  8.     private String username;  
  9.     private String password;  
  10.     private String gender;  
  11.     private Integer age;  
  12.     private Integer phoneID;  
  13.   
  14.     public Person() {  
  15.     }  
  16.   
  17.     public Person(Integer id, String username, String password, String gender,  
  18.             Integer age, Integer phoneID) {  
  19.         super();  
  20.         this.id = id;  
  21.         this.username = username;  
  22.         this.password = password;  
  23.         this.gender = gender;  
  24.         this.age = age;  
  25.         this.phoneID = phoneID;  
  26.     }  
  27.   
  28.     public Integer getId() {  
  29.         return id;  
  30.     }  
  31.   
  32.     public void setId(Integer id) {  
  33.         this.id = id;  
  34.     }  
  35.   
  36.     public String getUsername() {  
  37.         return username;  
  38.     }  
  39.   
  40.     public void setUsername(String username) {  
  41.         this.username = username;  
  42.     }  
  43.   
  44.     public String getPassword() {  
  45.         return password;  
  46.     }  
  47.   
  48.     public void setPassword(String password) {  
  49.         this.password = password;  
  50.     }  
  51.   
  52.     public String getGender() {  
  53.         return gender;  
  54.     }  
  55.   
  56.     public void setGender(String gender) {  
  57.         this.gender = gender;  
  58.     }  
  59.   
  60.     public Integer getAge() {  
  61.         return age;  
  62.     }  
  63.   
  64.     public void setAge(Integer age) {  
  65.         this.age = age;  
  66.     }  
  67.   
  68.     public Integer getPhoneID() {  
  69.         return phoneID;  
  70.     }  
  71.   
  72.     public void setPhoneID(Integer phoneID) {  
  73.         this.phoneID = phoneID;  
  74.     }  
  75.   
  76.     @Override  
  77.     public String toString() {  
  78.         return "Person [id=" + id + ", username=" + username + ", password="  
  79.                 + password + ", gender=" + gender + ", age=" + age  
  80.                 + ", phoneID=" + phoneID + "]";  
  81.     }  
  82.   
  83. }  
  1. package cn.wxy.domain;  
  2.   
  3. import java.io.Serializable;  
  4. import java.util.List;  
  5.   
  6. public class Phone implements Serializable {  
  7.     /**  
  8.      *   
  9.      */  
  10.     private static final long serialVersionUID = 3929743268466303948L;  
  11.     private Integer id;  
  12.     private String phoneNum;  
  13.     private List<Person> persons;  
  14.   
  15.     public Phone() {  
  16.     }  
  17.   
  18.     public Phone(Integer id, String phoneNum) {  
  19.         super();  
  20.         this.id = id;  
  21.         this.phoneNum = phoneNum;  
  22.     }  
  23.   
  24.     public Integer getId() {  
  25.         return id;  
  26.     }  
  27.   
  28.     public void setId(Integer id) {  
  29.         this.id = id;  
  30.     }  
  31.   
  32.     public String getPhoneNum() {  
  33.         return phoneNum;  
  34.     }  
  35.   
  36.     public void setPhoneNum(String phoneNum) {  
  37.         this.phoneNum = phoneNum;  
  38.     }  
  39.   
  40.     public List<Person> getPersons() {  
  41.         return persons;  
  42.     }  
  43.   
  44.     public void setPersons(List<Person> persons) {  
  45.         this.persons = persons;  
  46.     }  
  47.   
  48.     @Override  
  49.     public String toString() {  
  50.         return "Phone [id=" + id + ", phoneNum=" + phoneNum + "--------- persons="  
  51.                 + persons + "]";  
  52.     }  
  53.   
  54. }  

        mybatis有两种方式和数据库进行交互,一种是基于statement id的方式(映射需要提供get/set方法),另一种是基于mapper接口的方式(最后mapper接口也是调用Statement id方式,但是其映射只需要属性名和数据库字段名匹配即可),本处采用mapper接口方式作为测试代码与数据库进行交互。 两种方式的dao接口和测试代码也一样,不作为核心关注点,在此处列出。

  1. package cn.wxy.dao;  
  2.   
  3. import java.util.List;  
  4.   
  5. import cn.wxy.domain.Person;  
  6. import cn.wxy.domain.Phone;  
  7.   
  8. public interface PersonDao {  
  9.     /**  
  10.      * 根据phone id获取phone的信息,并把关联的Person对象查询出来  
  11.      * @param id  
  12.      * @return  
  13.      */  
  14.     Phone select(Integer id);  
  15. }  

  1. package cn.wxy.test;  
  2.   
  3.   
  4. import java.io.IOException;  
  5. import org.apache.ibatis.io.Resources;  
  6. import org.apache.ibatis.session.SqlSession;  
  7. import org.apache.ibatis.session.SqlSessionFactory;  
  8. import org.apache.ibatis.session.SqlSessionFactoryBuilder;  
  9. import org.junit.Before;  
  10. import org.junit.Test;  
  11. import cn.wxy.dao.PersonDao;  
  12. import cn.wxy.domain.Phone;  
  13.   
  14. public class PhoneTest {  
  15.     private SqlSessionFactory factory;  
  16.       
  17.     @Before  
  18.     public void setUp(){  
  19.         try {  
  20.             factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));  
  21.         } catch (IOException e) {  
  22.             e.printStackTrace();  
  23.         }  
  24.     }  
  25.   
  26.     @Test  
  27.     public void test() {  
  28.         SqlSession session = factory.openSession();  
  29.         PersonDao dao = session.getMapper(PersonDao.class);  
  30.         Phone phone = dao.select(2);  
  31.         System.out.println(phone);  
  32.         session.close();  
  33.     }  
  34.   
  35. }  

1. 第一种方式

        此种方式需要查询数据库两次,发出两次sql语句,一次是查询tbl_phone表,一次是查询tbl_person表,但是数据库返回的信息没有冗余,mapper.xml配置和测试代码如下。

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE mapper  
  3.   PUBLIC "-////DTD Mapper 3.0//EN"  
  4.   "http:///dtd/mybatis-3-mapper.dtd">  
  5. <mapper namespace="cn.wxy.dao.PersonDao">  
  6.     <resultMap type="Phone" id="selectPhomap">  
  7.         <id property="id" column="id"/>  
  8.         <result property="phoneNum" column="phoneNum"/>  
  9.         <collection property="persons" ofType="Person" select="selectPer" column="id">  
  10.             <id property="id" column="id"/>  
  11.             <result property="username" column="username"/>  
  12.             <result property="password" column="password"/>  
  13.             <result property="gender" column="gender"/>  
  14.             <result property="age" column="age"/>  
  15.             <result property="phoneID" column="phoneID"/>  
  16.         </collection>  
  17.     </resultMap>  
  18.     <select id="select" parameterType="int" resultMap="selectPhomap">  
  19.         select * from tbl_phone where id=#{id}  
  20.     </select>  
  21.     <select id="selectPer" parameterType="int" resultType="Person">  
  22.         select * from tbl_person where phoneID=#{phoneID}  
  23.     </select>  
  24. </mapper>  
        用直白的话来描述此种方式,在调用mapper接口中的select方法,先走语句“select * from tbl_phone where id=#{id}”,从数据库中拿到返回数据之后,开始做返回值映射,此时的返回值映射对象是一个resultMap(id:selectPhomap),该resultMap实际上是一个Phone实例,因此在开始做映射的时候,id和phoenNum因为属性名和数据库返回值一致完成映射,但是到了persons属性的时候,发现他是一个collection集合对象,里面存放的是Person实例,那怎么获取里面的数据?看collection标签的属性,他需要关联一个查询操作select="selectPer"获取其数据,即通过select * from tbl_person where phoneID=#{phoneID}获取collection中的数据,select="selectPer"操作需要传入数据,传入的数据column="id"即是第一次查询中返回的phone id,这个在这里显得不明显,在第二种方式中会更加的明显,这里也是mybatis关联查询的核心点,最后一步,就是将第二次查询出来的数据映射到collection中。详细过程,通过打印日志也可窥见一斑。

  1. 2015-04-29 16:02:15,188 DEBUG JdbcTransaction:132 - Opening JDBC Connection  
  2. 2015-04-29 16:02:15,536 DEBUG PooledDataSource:380 - Created connection 22429093.  
  3. 2015-04-29 16:02:15,537 DEBUG JdbcTransaction:98 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  4. 2015-04-29 16:02:15,539 DEBUG select:139 - ==>  Preparing: select * from tbl_phone where id=? //第一次发出sql语句  
  5. 2015-04-29 16:02:15,586 DEBUG select:139 - ==> Parameters: 2(Integer)  
  6. 2015-04-29 16:02:15,616 DEBUG selectPer:139 - ====>  Preparing: select * from tbl_person where phoneID=? //第二次发出sql语句  
  7. 2015-04-29 16:02:15,617 DEBUG selectPer:139 - ====> Parameters: 2(Integer)//关注点:第二次查询参数的传入  
  8. 2015-04-29 16:02:15,621 DEBUG selectPer:139 - <====      Total: 2  
  9. 2015-04-29 16:02:15,622 DEBUG select:139 - <==      Total: 1  
  10. Phone [id=2, phoneNum=15200898242, persons=[Person [id=1, username=zhangsan, password=123, gender=male, age=18, phoneID=2], Person [id=2, username=lisi, password=123, gender=female, age=24, phoneID=2]]]  
  11. 2015-04-29 16:02:15,623 DEBUG JdbcTransaction:120 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  12. 2015-04-29 16:02:15,624 DEBUG JdbcTransaction:88 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  13. 2015-04-29 16:02:15,625 DEBUG PooledDataSource:334 - Returned connection 22429093 to pool.  

        总结:

        1. 将查询操作拆成两步来完成;

        2. 核心点:第二次查询操作传入的数据通过collection的属性column来传入,此时column的value需要和第一次查询中的返回值做映射,映射的规则是数据库返回值的name和属性name相等;

2. 第二种方式

        此种方式只需要查询数据库一次,发出一次sql语句,但是数据库返回值存在冗余,冗余内容为phone的信息,冗余次数是Person中使用统一电话的人的个数。mapper.xml配置文件内容如下。

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE mapper  
  3.   PUBLIC "-////DTD Mapper 3.0//EN"  
  4.   "http:///dtd/mybatis-3-mapper.dtd">  
  5. <mapper namespace="cn.wxy.dao.PersonDao">  
  6.     <resultMap type="Phone" id="selectPhomap">  
  7.         <id property="id" column="phoID"/>  
  8.         <result property="phoneNum" column="phoneNum"/>  
  9.         <collection property="persons" ofType="Person">  
  10.             <id property="id" column="id"/>  
  11.             <result property="username" column="username"/>  
  12.             <result property="password" column="password"/>  
  13.             <result property="gender" column="gender"/>  
  14.             <result property="age" column="age"/>  
  15.             <result property="phoneID" column="phoneID"/>  
  16.         </collection>  
  17.     </resultMap>  
  18.     <select id="select" parameterType="int" resultMap="selectPhomap">  
  19.         select pho.id phoID, pho.phoneNum, per.* from tbl_phone pho, tbl_person per  
  20.         where pho.id=#{id}  
  21.         and pho.id=per.phoneID  
  22.     </select>  
  23. </mapper>  
        通过此种方式实现关联查询,sql语句执行的返回值就已经包含了所有需要映射的数据,不需要再进行二次查询,但是存在一个问题,就是两张表的id字段的命名是一样的,所以在做关联查询的时候,需要将其中一个取别名以示区别,否则在映射的时候会出现不和实际情况的异常——sql和参数映射的时候是正确的,解析也是正确的,到数据库执行是正确的,数据库的返回值也是正确的,但是sql返回的是多条完整的数据记录,在做返回值映射的时候,因为phone和Person的主键都是id,一开始做映射的时候都被赋予值2,因为已经映射过了,所以第二条数据就不会在被映射。如以下所示的错误信息,person的信息其实是person.id=1的信息,但是其id=2,其他的信息都是id=1的数据。

  1. 2015-04-29 16:24:37,892 DEBUG JdbcTransaction:132 - Opening JDBC Connection  
  2. 2015-04-29 16:24:38,194 DEBUG PooledDataSource:380 - Created connection 289639718.  
  3. 2015-04-29 16:24:38,195 DEBUG JdbcTransaction:98 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@11438d26]  
  4. 2015-04-29 16:24:38,198 DEBUG select:139 - ==>  Preparing: select pho.id, pho.phoneNum, per.* from tbl_phone pho, tbl_person per where pho.id=? and pho.id=per.phoneID   
  5. 2015-04-29 16:24:38,239 DEBUG select:139 - ==> Parameters: 2(Integer)  
  6. 2015-04-29 16:24:38,290 DEBUG select:139 - <==      Total: 2  
  7. Phone [id=2, phoneNum=15200898242--------- persons=[Person [id=2, username=zhangsan, password=123, gender=male, age=18, phoneID=2]]]  
  8. 2015-04-29 16:24:38,292 DEBUG JdbcTransaction:120 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@11438d26]  
  9. 2015-04-29 16:24:38,293 DEBUG JdbcTransaction:88 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@11438d26]  
  10. 2015-04-29 16:24:38,293 DEBUG PooledDataSource:334 - Returned connection 289639718 to pool.  
        实际上要想了解此种方式的异常原因,需要从数据库返回值入手,在数据库中直接执行mapper.xml中的sql语句,其返回的数据组织情况如下图所示。


        如果没有修改其中某一个id别名,那么mybatis在做结果映射的时候会发现两次的映射主键是一样的,因此第二条记录就不会被映射了。正确执行结果如下所示。

  1. 2015-04-29 18:27:42,758 DEBUG JdbcTransaction:132 - Opening JDBC Connection  
  2. 2015-04-29 18:27:43,117 DEBUG PooledDataSource:380 - Created connection 22429093.  
  3. 2015-04-29 18:27:43,117 DEBUG JdbcTransaction:98 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  4. 2015-04-29 18:27:43,133 DEBUG select:139 - ==>  Preparing: select pho.id phoID, pho.phoneNum, per.* from tbl_phone pho, tbl_person per where pho.id=? and pho.id=per.phoneID   
  5. 2015-04-29 18:27:43,164 DEBUG select:139 - ==> Parameters: 2(Integer)  
  6. 2015-04-29 18:27:43,211 DEBUG select:139 - <==      Total: 2  
  7. Phone [id=2, phoneNum=15200898242--------- persons=[Person [id=1, username=zhangsan, password=123, gender=male, age=18, phoneID=2], Person [id=2, username=lisi, password=123, gender=female, age=24, phoneID=2]]]  
  8. 2015-04-29 18:27:43,211 DEBUG JdbcTransaction:120 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  9. 2015-04-29 18:27:43,211 DEBUG JdbcTransaction:88 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1563da5]  
  10. 2015-04-29 18:27:43,211 DEBUG PooledDataSource:334 - Returned connection 22429093 to pool.  
        总结:

        1. 查询一步完成,关注sql的编写;

        2. 注意sql返回之中是否存在同名的列,如果存在同名列,则mybatis做结果映射的时候有可能发生异常,需要取别名;


三、编码实现

1. 第一种实现方式

        示例代码下载:点击打开链接

2. 第二种实现方式

        示例代码下载:点击打开链接



附注:

        本文如有错漏,烦请不吝指正,谢谢!

        示例代码简陋,包涵!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多