{T}

多表、嵌套查询与加载策略

多表查询

案例环境准备

sql
DROP TABLE IF EXISTS orders; 
CREATE TABLE orders (
  id INT (11) NOT NULL AUTO_INCREMENT,
  ordertime VARCHAR (255) DEFAULT NULL,
  total DOUBLE DEFAULT NULL,
  uid INT (11) DEFAULT NULL,
  PRIMARY KEY (id),
  KEY uid (uid),CONSTRAINT orders_ibfk_1 FOREIGN KEY (uid) REFERENCES user (id)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 

-- Records of orders
INSERT INTO orders VALUES ('1','2020-12-12','3000','1'); 
INSERT INTO orders VALUES ('2','2020-12-12','4000','1'); 
INSERT INTO orders VALUES ('3','2020-12-12','5000','2'); 


DROP TABLE IF EXISTS sys_role; 
CREATE TABLE sys_role (
  id INT (11) NOT NULL AUTO_INCREMENT,
  rolename VARCHAR (255) DEFAULT NULL,
  roleDesc VARCHAR (255) DEFAULT NULL,
  PRIMARY KEY (id)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; 

-- Table structure for sys_role
INSERT INTO sys_role VALUES ('1','CTO','CTO'); 
INSERT INTO sys_role VALUES ('2','CEO','CEO'); 

DROP TABLE IF EXISTS sys_user_role; 
CREATE TABLE sys_user_role (
  userid INT (11) NOT NULL,
  roleid INT (11) NOT NULL,
  PRIMARY KEY (userid,roleid),
  KEY roleid (roleid),
  CONSTRAINT sys_user_role_ibfk_1 FOREIGN KEY (userid) REFERENCES sys_role (id),
  CONSTRAINT sys_user_role_ibfk_2 FOREIGN KEY (roleid) REFERENCES user (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8; 

-- Records of sys_user_role
INSERT INTO sys_user_role VALUES ('1','1'); 
INSERT INTO sys_user_role VALUES ('2','1'); 
INSERT INTO sys_user_role VALUES ('1','2'); 
INSERT INTO sys_user_role VALUES ('2','2');

一对一(多对一)

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询所有订单,与此同时查询出每个订单所属的用户

sql
SELECT * FROM orders o LEFT JOIN USER u ON o.uid=u.id;

代码实现

1 )Order实体

java
public class Order { 
  private Integer id; 
  private Date ordertime; 
  private double money; 
  // 表示当前订单属于哪个用户 
  private User user; 
}

2 )OrderMapper接口

java
public interface OrderMapper { 
  public List<Order> findAllWithUser(); 
}

3 )OrderMapper.xml映射

xml
<!-- 一对一关联查询:查询所有订单,与此同时还要查询出每个订单所属的用户信息 -->
<resultMap id="orderMap" type="com.lagou.domain.Orders">
  <id property="id" column="id"/>
  <result property="ordertime" column="ordertime"/>
  <result property="total" column="total"/>
  <result property="uid" column="uid"/>


  <!--
   association: 在进行一对一关联查询配置时,使用 association 标签进行关联
   property="user" :要封装实体的属性名
   javaType="com.lagou.domain.User" 要封装的实体的属性类型
  -->
  <association property="user" javaType="com.lagou.domain.User">
    <id property="id" column="uid"></id>
    <result property="username" column="username"></result>
    <result property="birthday" column="birthday"></result>
    <result property="sex" column="sex"></result>
    <result property="address" column="address"></result>
  </association>
</resultMap>

<select id="findAllWithUser" resultMap="orderMap">
  SELECT * FROM orders o LEFT JOIN USER u ON o.uid = u.id
</select>

4 )测试代码

java
/*
一对一关联查询:查询所有订单,与此同时还要查询出每个订单所属的用户信息
*/
@org.testng.annotations.Test
public void test1() throws IOException {

  InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
  SqlSession sqlSession = sqlSessionFactory.openSession();
  OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
  List<Orders> orders = mapper.findAllWithUser();

  for (Orders order : orders) {
    System.out.println(order);
  }

  sqlSession.close();
}

一对多

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询所有用户,与此同时查询出该用户具有的订单

sql
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.id = o.uid;

代码实现

1 )User 实体

java
public class User { 
  private Integer id; 
  private String username; 
  private Date birthday; 
  private String sex; 
  private String address; 
  // 代表当前用户具备的订单列表 
  private List<Order> orderList; 
}

2 )UserMapper 接口

java
public interface UserMapper { 
    public List<User> findAllWithOrder();
}

3 )UserMapper.xml 映射

xml
<!--一对多关联查询:查询所有的用户,同时还要查询出每个用户所关联的订单信息-->
<resultMap id="userMap" type="com.lagou.domain.User">
  <id property="id" column="id"></id>
  <result property="username" column="username"></result>
  <result property="birthday" column="birthday"></result>
  <result property="sex" column="sex"></result>
  <result property="address" column="address"></result>

  <!-- collection : 一对多使用collection标签进行关联 -->
  <collection property="ordersList" ofType="com.lagou.domain.Orders">
    <id property="id" column="oid"></id>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>
  </collection>
</resultMap>

<!-- o.id oid 这是起别名 -->
<select id="findAllWithOrder"  resultMap="userMap">
  SELECT u.*,o.id oid,o.ordertime,o.total,o.uid FROM orders o RIGHT JOIN USER u ON o.uid = u.id
</select>

4 )测试代码

java
// 一对多关联查询:查询所有用户及关联的订单信息
@org.testng.annotations.Test
public void test2() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<User> allWithOrder = mapper.findAllWithOrder();

    for (User user : allWithOrder) {
      System.out.println(user);
    }
  
    sqlSession.close();
}

多对多

用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用

多对多查询的需求:查询所有用户同时查询出该用户的所有角色

多对多查询语句

sql
SELECT * FROM USER u -- 用户表 
LEFT JOIN user_role ur -- 左外连接中间表 
ON u.id = ur.uid 
LEFT JOIN role r -- 左外连接中间表 
ON ur.rid = r.id ;

代码实现

1 )User 和 Role 实体

java
public class User { 
  private Integer id; 
  private String username; 
  private Date birthday; 
  private String sex; 
  private String address; 
  // 代表当前用户关联的角色列表 
  private List<Role> roleList; 
}

public class Role { 
  private Integer id; 
  private String roleName; 
  private String roleDesc; 
}

2 )UserMapper 接口

java
public interface UserMapper { 
	public List<User> findAllWithRole(); 
}

3 )UserMapper.xml映射

xml
<!--多对多关联查询:查询所有的用户,同时还要查询出每个用户所关联的角色信息-->

<resultMap id="userRoleMap" type="user">
  <id property="id" column="id"/>
  <result property="username" column="username"></result>
  <result property="birthday" column="birthday"></result>
  <result property="sex" column="sex"></result>
  <result property="address" column="address"></result>

  <collection property="roleList" ofType="role">
    <id column="rid" property="id"></id>
    <result column="rolename" property="rolename"></result>
    <result column="roleDesc" property="roleDesc"></result>
  </collection>

</resultMap>

<select id="findAllWithRole" resultMap="userRoleMap">
  SELECT u.*, r.id rid, r.rolename, r.roleDesc FROM USER u
  LEFT JOIN sys_user_role ur ON ur.userid = u.id
  LEFT JOIN sys_role r ON ur.roleid = r.id
</select>

测试代码

java
@Test 
public void testUserWithRole() throws Exception { 
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  List<User> list = userMapper.findAllWithRole(); 
  for (User user : list) { 
    System.out.println(user); 
  } 
}

嵌套查询

嵌套查询就是将原来多表查询中的联合查询语句拆成单个表的查询,再使用 mybatis 的语法嵌套在一起

需求:查询一个订单,与此同时查询出该订单所属的用户

  • 联合查询

  • SELECT * FROM orders o LEFT JOIN USER u ON o.uid=u.id;

  • 嵌套查询

  • 先查询订单

  • SELECT * FROM orders

  • 再根据订单 uid 外键,查询用户

  • SELECT * FROM user WHERE id = #{根据订单查询的uid}

  • 最后使用 mybatis,将以上二步嵌套起来

一对一嵌套查询

需求:查询一个订单,与此同时查询出该订单所属的用户

一对一查询语句

  • 先查询订单

  • SELECT * FROM orders;

  • 再根据订单uid外键,查询用户

  • SELECT * FROM user WHERE id = #{订单的uid};

代码实现

1)OrderMapper 接口

java
// 一对一嵌套查询:查询所有订单,与此同时还要查询出每个订单所属的用户信息
public List<Orders> findAllWithUser2();

2)OrderMapper.xml 映射

xml
<resultMap id="orderMap2" type="com.lagou.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>

    <!-- 问题:1.怎么去执行第二条 sql , 2.如何执行第二条 sql 的时候,把 uid 作为参数进行传递 -->
    <association property="user" javaType="com.lagou.domain.User"
                     select="com.lagou.mapper.UserMapper.findById" column="uid" fetchType="eager"/>
</resultMap>

<!--一对一嵌套查询-->
<select id="findAllWithUser2" resultMap="orderMap2">
    SELECT * FROM orders
</select>

<select id="findByUid" parameterType="int" resultType="com.lagou.domain.Orders">
    SELECT * FROM orders WHERE uid = #{uid}
</select>

一对多嵌套查询

需求:查询所有用户,与此同时查询出该用户具有的订单

一对多查询语句

  • 先查询用户

  • SELECT * FROM user;

  • 再根据用户id主键,查询订单列表

  • SELECT * FROM orders where uid = #{用户id};

代码实现

java
public interface UserMapper { 
  public List<User> findAllWithOrder(); 
}
<!-- 一对多嵌套查询:查询所有的用户,同时还要查询出每个用户所关联的订单信息-->
<resultMap id="userOrderMap" type="com.lagou.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"></result>
    <result property="birthday" column="birthday"></result>
    <result property="sex" column="sex"></result>
    <result property="address" column="address"></result>

    <!-- fetchType="lazy" : 延迟加载策略  fetchType="eager": 立即加载策略 -->
    <collection property="ordersList" ofType="com.lagou.domain.Orders" column="id"
                    select="com.lagou.mapper.OrderMapper.findByUid">
  	</collection>

</resultMap>

<select id="findAllWithOrder2" resultMap="userOrderMap">
    SELECT * FROM USER
</select>

<select id="findByUid" parameterType="int" resultType="com.lagou.domain.Orders">
    SELECT * FROM orders WHERE uid = #{uid}
</select>

OrderMapper 接口

java
public interface OrderMapper { 
    public List<Order> findByUid(Integer uid); 
}
<!-- com.lagou.mapper.OrderMapper.findByUid -->
<select id="findByUid" parameterType="int" resultType="com.lagou.domain.Orders">
    SELECT * FROM orders WHERE uid = #{uid}
</select>

多对多嵌套查询

需求:查询用户 同时查询出该用户的所有角色

多对多查询语句

  • 先查询用户
  • SELECT * FROM user;
  • 再根据用户 id 主键,查询角色列表
  • SELECT * FROM role r INNER JOIN user_role ur ON r.id= ur.rid WHERE ur.uid = #{用户id};

代码实现

a)UserMapper接口

java
public interface UserMapper { 
  public List<User> findAllWithRole(); 
}

b)UserMapper.xml映射

xml
 <resultMap id="userRoleMap2" type="com.lagou.domain.User">
     <id property="id" column="id"/>
     <result property="username" column="username"></result>
     <result property="birthday" column="birthday"></result>
     <result property="sex" column="sex"></result>
   	 <result property="address" column="address"></result>
       
     <collection property="roleList" ofType="com.lagou.domain.Role" column="id"
                 select="com.lagou.mapper.RoleMapper.findByUid">
      </collection>
</resultMap>

<!--多对多嵌套查询:查询所有的用户,同时还要查询出每个用户所关联的角色信息-->
<select id="findAllWithRole2" resultMap="userRoleMap2">
    SELECT * FROM USER
</select>


<!-- RoleMapper -->
<select id="findByUid" parameterType="int" resultType="role"> 
  SELECT r.id,r.role_name roleName,r.role_desc roleDesc FROM role r INNER JOIN user_role ur ON r.id = ur.rid 
  WHERE ur.uid = #{uid} 
</select>

c)RoleMapper 接口

java
public interface RoleMapper { 
  public List<Role> findByUid(Integer uid); 
}

d)RoleMapper.xml 映射

xml
<select id="findByUid" parameterType="int" resultType="role"> 
  SELECT r.id,r.role_name roleName,r.role_desc roleDesc FROM role r INNER JOIN user_role ur ON r.id = ur.rid 
  WHERE ur.uid = #{uid} 
</select>

e)测试代码

java
@Test 
public void testUserWithRole() throws Exception { 
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 
  List<User> list = userMapper.findAllWithRole(); 
  for (User user : list) { 
    System.out.println(user);
  } 
}

加载策略

延迟加载

通过前面的学习,我们已经掌握了 Mybatis 中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我们所说的延迟加载

举个栗子

  • 在一对多中,当我们有一个用户,它有个100个订单

  • 在查询用户的时候,要不要把关联的订单查出来?(不要)

  • 在查询订单的时候,要不要把关联的用户查出来?(要)

  • 回答

  • 在查询用户时,用户下的订单应该是,什么时候用,什么时候查询。

  • 在查询订单时,订单所属的用户信息应该是随着订单一起查询出来。

延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载

  • 优点:

  • 先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快

  • 缺点:

  • 因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降

  • 在多表中:

  • 一对多,多对多:通常情况下采用延迟加载

  • 一对一(多对一):通常情况下采用立即加载

  • 注意:

  • 延迟加载是基于嵌套查询来实现的

局部延迟加载

在 association 和 collection 标签中都有一个 etchType 属性,通过修改它的值,可以修改局部的加载策略

xml
<!-- 开启一对多 延迟加载 --> 
<resultMap id="userMap" type="user">
  <id column="id" property="id"></id> 
  <result column="username" property="username"></result> 
  <result column="password" property="password"></result> 
  <result column="birthday" property="birthday"></result> 
  
  <!--fetchType="lazy" 懒加载策略 fetchType="eager" 立即加载策略 --> 
  <collection property="orderList" ofType="order" column="id" 
              select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy"> 
  </collection> 
</resultMap> 

<select id="findAll" resultMap="userMap"> 
  SELECT * FROM user
</select>

设置触发延迟加载的方法

大家在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的 equals、clone、hashCode、toString 方法时也会触发关联对象的查询

我们可以在配置文件中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。没有查询的字段,默认为null

xml
<settings> 
  <!--所有方法都会延迟加载--> 
  <setting name="lazyLoadTriggerMethods" value="toString()"/> 
</settings>

全局延迟加载

在 Mybatis 的核心配置文件中可以使用 setting 标签修改全局的加载策略

xml
<settings> 
  <!--开启全局延迟加载功能--> 
  <setting name="lazyLoadingEnabled" value="true"/> 
</settings>

注意: 局部的加载策略优先级高于全局的加载策略

版本差异(旧版 → MyBatis 3.5.x)

特性旧版(MyBatis 3.x 早期)MyBatis 3.5.x
全局配置lazyLoadingEnabled / aggressiveLazyLoading机制不变;3.4.1+ aggressiveLazyLoading 默认改为 false
局部加载策略@One/@Many 的 fetchType用法不变
嵌套查询一对一/一对多 association/collection用法不变
性能调优联表查询 vs 嵌套查询取舍不变;建议结合 N+1 问题排查
record 支持3.5.13+ 支持 record 构造器自动映射

加载策略相关配置(lazyLoadingEnabled、aggressiveLazyLoading、fetchType)在 3.5.x 中保持兼容,本文示例可直接运行。