25 | 经典的 N+1 SQL 问题如何正确解决?(上)

在 JPA 的使用过程中,N+1 SQL 是很常见的问题,相信很多程序员都遇到过这一问题,我看见很多同事处理起来束手无策,那么它究竟真的有那么麻烦吗?这一讲我会帮助你梳理思路,看看到底如何解决这个经典问题。

注:由于内容较多,我将这部分内容拆分成两讲,方便你学习。

什么是 N+1 SQL 问题?

想要解决一个问题,必须要知道它是什么、如何产生的,这样才能有方法、有逻辑地去解决它。下面通过一个例子来看一下什么是 N+1 的 SQL 问题。

假设一个 UserInfo 实体对象和 Address 是一对多的关系,即一个用户有多个地址,我们首先看一下一般实体里面的关联关系会怎么写。两个实体对象如下述代码所示。

复制代码
  1. //UserInfo实体对象如下:
  2. @Entity
  3. @Data
  4. @SuperBuilder
  5. @AllArgsConstructor
  6. @NoArgsConstructor
  7. @Table
  8. @ToString(exclude = "addressList")//exclued防止 toString打印日志的时候死循环
  9. public class UserInfo extends BaseEntity {
  10. private String name;
  11. private String telephone;
  12. // UserInfo实体对象的关联关系由Address对象里面的userInfo字段维护,默认是lazy加载模式,为了方便演示fetch取EAGER模式。此处是一对多关联关系
  13. @OneToMany(mappedBy = "userInfo",fetch = FetchType.EAGER)
  14. private List<Address> addressList;
  15. }
  16. //Address对象如下:
  17. @Entity
  18. @Table
  19. @Data
  20. @SuperBuilder
  21. @AllArgsConstructor
  22. @NoArgsConstructor
  23. @ToString(exclude = "userInfo")
  24. public class Address extends BaseEntity {
  25. private String city;
  26. //维护UserInfo和Address的外键关系,方便演示也采用EAGER模式;
  27. @ManyToOne(fetch = FetchType.EAGER)
  28. @JsonBackReference //此注解防止JSON死循环
  29. private UserInfo userInfo;
  30. }

其次,我们假设数据库里面有三条 UserInfo 的数据,ID 分别为 3、6、9,如下图所示。

其中,每个 UserInfo 分别有两条 Address 数据,也就是一共 6 条 Address 的数据,如下图所示。

然后,我们请求通过 UserInfoRepository 查询所有的 UserInfo 信息,方法如下面这行代码所示。

复制代码
  1. userInfoRepository.findAll()

现在,我们的控制台将会得到四个 SQL,如下所示。

复制代码
  1. org.hibernate.SQL :
  2. select userinfo0_.id as id1_1_,
  3. userinfo0_.create_time as create_t2_1_,
  4. userinfo0_.create_user_id as create_u3_1_,
  5. userinfo0_.last_modified_time as last_mod4_1_,
  6. userinfo0_.last_modified_user_id as last_mod5_1_,
  7. userinfo0_.version as version6_1_,
  8. userinfo0_.ages as ages7_1_,
  9. userinfo0_.email_address as email_ad8_1_,
  10. userinfo0_.last_name as last_nam9_1_,
  11. userinfo0_.name as name10_1_,
  12. userinfo0_.telephone as telepho11_1_
  13. from user_info userinfo0_ org.hibernate.SQL :
  14. select addresslis0_.user_info_id as user_inf8_0_0_,
  15. addresslis0_.id as id1_0_0_,
  16. addresslis0_.id as id1_0_1_,
  17. addresslis0_.create_time as create_t2_0_1_,
  18. addresslis0_.create_user_id as create_u3_0_1_,
  19. addresslis0_.last_modified_time as last_mod4_0_1_,
  20. addresslis0_.last_modified_user_id as last_mod5_0_1_,
  21. addresslis0_.version as version6_0_1_,
  22. addresslis0_.city as city7_0_1_,
  23. addresslis0_.user_info_id as user_inf8_0_1_
  24. from address addresslis0_
  25. where addresslis0_.user_info_id = ? org.hibernate.SQL :
  26. select addresslis0_.user_info_id as user_inf8_0_0_,
  27. addresslis0_.id as id1_0_0_,
  28. addresslis0_.id as id1_0_1_,
  29. addresslis0_.create_time as create_t2_0_1_,
  30. addresslis0_.create_user_id as create_u3_0_1_,
  31. addresslis0_.last_modified_time as last_mod4_0_1_,
  32. addresslis0_.last_modified_user_id as last_mod5_0_1_,
  33. addresslis0_.version as version6_0_1_,
  34. addresslis0_.city as city7_0_1_,
  35. addresslis0_.user_info_id as user_inf8_0_1_
  36. from address addresslis0_
  37. where addresslis0_.user_info_id = ? org.hibernate.SQL :
  38. select addresslis0_.user_info_id as user_inf8_0_0_,
  39. addresslis0_.id as id1_0_0_,
  40. addresslis0_.id as id1_0_1_,
  41. addresslis0_.create_time as create_t2_0_1_,
  42. addresslis0_.create_user_id as create_u3_0_1_,
  43. addresslis0_.last_modified_time as last_mod4_0_1_,
  44. addresslis0_.last_modified_user_id as last_mod5_0_1_,
  45. addresslis0_.version as version6_0_1_,
  46. addresslis0_.city as city7_0_1_,
  47. addresslis0_.user_info_id as user_inf8_0_1_
  48. from address addresslis0_
  49. where addresslis0_.user_info_id = ?

通过 SQL 我们可以看得出来,当取 UserInfo 的时候,有多少条 UserInfo 数据就会触发多少条查询 Address 的 SQL。

那么所谓的 N+1 的 SQL,此时 1 代表的是一条 SQL 查询 UserInfo 信息;N 条 SQL 查询 Address 的信息。你可以想象一下,如果有 100 条 UserInfo 信息,可能会触发 100 条查询 Address 的 SQL,性能多差呀。

很简单,这就是我们常说的 N+1 SQL 问题。我们这里使用的是 EAGER 模式,当使用 LAZY 的时候也是一样的道理,只是生成 N 条 SQL 的时机是不一样的。

上面我演示了 @OneToMany 的情况,那么我们再看一下 @ManyToOne 的情况。利用 AddressRepository 查询所有的 Address 信息,方法如下面这行代码所示。

复制代码
  1. addressRepository.findAll();

这个时候我们再看一下控制台,会产生如下 SQL。

复制代码
  1. org.hibernate.SQL :
  2. select address0_.id as id1_0_,
  3. address0_.create_time as create_t2_0_,
  4. address0_.create_user_id as create_u3_0_,
  5. address0_.last_modified_time as last_mod4_0_,
  6. address0_.last_modified_user_id as last_mod5_0_,
  7. address0_.version as version6_0_,
  8. address0_.city as city7_0_,
  9. address0_.user_info_id as user_inf8_0_
  10. from address address0_
  11. org.hibernate.SQL :
  12. select userinfo0_.id as id1_1_0_,
  13. userinfo0_.create_time as create_t2_1_0_,
  14. userinfo0_.create_user_id as create_u3_1_0_,
  15. userinfo0_.last_modified_time as last_mod4_1_0_,
  16. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  17. userinfo0_.version as version6_1_0_,
  18. userinfo0_.ages as ages7_1_0_,
  19. userinfo0_.email_address as email_ad8_1_0_,
  20. userinfo0_.last_name as last_nam9_1_0_,
  21. userinfo0_.name as name10_1_0_,
  22. userinfo0_.telephone as telepho11_1_0_,
  23. addresslis1_.user_info_id as user_inf8_0_1_,
  24. addresslis1_.id as id1_0_1_,
  25. addresslis1_.id as id1_0_2_,
  26. addresslis1_.create_time as create_t2_0_2_,
  27. addresslis1_.create_user_id as create_u3_0_2_,
  28. addresslis1_.last_modified_time as last_mod4_0_2_,
  29. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  30. addresslis1_.version as version6_0_2_,
  31. addresslis1_.city as city7_0_2_,
  32. addresslis1_.user_info_id as user_inf8_0_2_
  33. from user_info userinfo0_
  34. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  35. where userinfo0_.id = ?
  36. org.hibernate.SQL :
  37. select userinfo0_.id as id1_1_0_,
  38. userinfo0_.create_time as create_t2_1_0_,
  39. userinfo0_.create_user_id as create_u3_1_0_,
  40. userinfo0_.last_modified_time as last_mod4_1_0_,
  41. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  42. userinfo0_.version as version6_1_0_,
  43. userinfo0_.ages as ages7_1_0_,
  44. userinfo0_.email_address as email_ad8_1_0_,
  45. userinfo0_.last_name as last_nam9_1_0_,
  46. userinfo0_.name as name10_1_0_,
  47. userinfo0_.telephone as telepho11_1_0_,
  48. addresslis1_.user_info_id as user_inf8_0_1_,
  49. addresslis1_.id as id1_0_1_,
  50. addresslis1_.id as id1_0_2_,
  51. addresslis1_.create_time as create_t2_0_2_,
  52. addresslis1_.create_user_id as create_u3_0_2_,
  53. addresslis1_.last_modified_time as last_mod4_0_2_,
  54. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  55. addresslis1_.version as version6_0_2_,
  56. addresslis1_.city as city7_0_2_,
  57. addresslis1_.user_info_id as user_inf8_0_2_
  58. from user_info userinfo0_
  59. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  60. where userinfo0_.id = ?
  61. org.hibernate.SQL :
  62. select userinfo0_.id as id1_1_0_,
  63. userinfo0_.create_time as create_t2_1_0_,
  64. userinfo0_.create_user_id as create_u3_1_0_,
  65. userinfo0_.last_modified_time as last_mod4_1_0_,
  66. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  67. userinfo0_.version as version6_1_0_,
  68. userinfo0_.ages as ages7_1_0_,
  69. userinfo0_.email_address as email_ad8_1_0_,
  70. userinfo0_.last_name as last_nam9_1_0_,
  71. userinfo0_.name as name10_1_0_,
  72. userinfo0_.telephone as telepho11_1_0_,
  73. addresslis1_.user_info_id as user_inf8_0_1_,
  74. addresslis1_.id as id1_0_1_,
  75. addresslis1_.id as id1_0_2_,
  76. addresslis1_.create_time as create_t2_0_2_,
  77. addresslis1_.create_user_id as create_u3_0_2_,
  78. addresslis1_.last_modified_time as last_mod4_0_2_,
  79. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  80. addresslis1_.version as version6_0_2_,
  81. addresslis1_.city as city7_0_2_,
  82. addresslis1_.user_info_id as user_inf8_0_2_
  83. from user_info userinfo0_
  84. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  85. where userinfo0_.id = ?

这里通过 SQL 我们可以看得出来,当取 Address 的时候,Address 里面有多少个 user_info_id,就会触发多少条查询 UserInfo 的 SQL。

那么所谓的 N+1 的 SQL,此时 1 就代表一条 SQL 查询 Address 信息;N 条 SQL 查询 UserInfo 的信息。同样,你可以想象一下,如果我们有 100 条 Address 信息,分别有不同的 user_info_id 可能会触发 100 条查询 UserInfo 的 SQL,性能依然很差。

这也是我们常说的 N+1 SQL 问题,我只是给你演示了 @OneToMany 和 @ManyToOne 的情况,@ManyToMany 和 @OneToOne 也是一样的道理,都是当我们查询主体信息时候,1 条 SQL 会衍生出来关联关系的 N 条 SQL。

现在你认识了这个问题,下一步该思考,怎么解决才更合理呢?有没有什么办法可以减少 SQL 条数呢?

减少 N+1 SQL 的条数

最容易想到,就是有没有什么机制可以减少 N 对应的 SQL 条数呢?从原理分析会知道,不管是 LAZY 还是 EAGER 都是没有用的,因为这两个只是决定了 N 条 SQL 的触发时机,而不能减少 SQL 的条数。

不知道你是否还记得在第 20 讲(Spring JPA 中的 Hibernate 加载过程与配置项是怎么回事)中,我们介绍过的 Hibernate 的配置项有哪些,如果你回过头去看,会发现有个配置可以改变每次批量取数据的大小。

hibernate.default_batch_fetch_size 配置

hibernate.default_batch_fetch_size 配置在 AvailableSettings.class 里面,指的是批量获取数据的大小,默认是 -1,表示默认没有匹配取数据。那么我们把这个值改成 20 看一下效果,只需要在 application.properties 里面增加如下配置即可。

复制代码
  1. # 更改批量取数据的大小为20
  2. spring.jpa.properties.hibernate.default_batch_fetch_size= 20

在实体类不发生任何改变的前提下,我们再执行如下两个方法,分别看一下 SQL 的生成情况。

复制代码
  1. userInfoRepository.findAll();

还是先查询所有的 UserInfo 信息,看一下 SQL 的执行情况,代码如下所示。

复制代码
  1. org.hibernate.SQL :
  2. select userinfo0_.id as id1_1_,
  3. userinfo0_.create_time as create_t2_1_,
  4. userinfo0_.create_user_id as create_u3_1_,
  5. userinfo0_.last_modified_time as last_mod4_1_,
  6. userinfo0_.last_modified_user_id as last_mod5_1_,
  7. userinfo0_.version as version6_1_,
  8. userinfo0_.ages as ages7_1_,
  9. userinfo0_.email_address as email_ad8_1_,
  10. userinfo0_.last_name as last_nam9_1_,
  11. userinfo0_.name as name10_1_,
  12. userinfo0_.telephone as telepho11_1_
  13. from user_info userinfo0_ org.hibernate.SQL :
  14. select addresslis0_.user_info_id as user_inf8_0_1_,
  15. addresslis0_.id as id1_0_1_,
  16. addresslis0_.id as id1_0_0_,
  17. addresslis0_.create_time as create_t2_0_0_,
  18. addresslis0_.create_user_id as create_u3_0_0_,
  19. addresslis0_.last_modified_time as last_mod4_0_0_,
  20. addresslis0_.last_modified_user_id as last_mod5_0_0_,
  21. addresslis0_.version as version6_0_0_,
  22. addresslis0_.city as city7_0_0_,
  23. addresslis0_.user_info_id as user_inf8_0_0_
  24. from address addresslis0_
  25. where addresslis0_.user_info_id in (?, ?, ?)

我们可以看到 SQL 直接减少到两条了,其中查询 Address 的地方查询条件变成了 in(?,?,?)。

想象一下,如果我们有 20 条 UserInfo 信息,那么产生的 SQL 也是两条,此时要比 20+1 条 SQL 性能高太多了。

接着我们再执行另一个方法,看一下 @ManyToOne 的情况,代码如下所示。

复制代码
  1. addressRepository.findAll()

关于执行的 SQL 情况如下所示。

复制代码
  1. 2020-11-29 23:11:27.381 DEBUG 30870 --- [nio-8087-exec-5] org.hibernate.SQL                        :
  2. select address0_.id as id1_0_,
  3. address0_.create_time as create_t2_0_,
  4. address0_.create_user_id as create_u3_0_,
  5. address0_.last_modified_time as last_mod4_0_,
  6. address0_.last_modified_user_id as last_mod5_0_,
  7. address0_.version as version6_0_,
  8. address0_.city as city7_0_,
  9. address0_.user_info_id as user_inf8_0_
  10. from address address0_
  11. 2020-11-29 23:11:27.383 DEBUG 30870 --- [nio-8087-exec-5] org.hibernate.SQL                        :
  12. select userinfo0_.id as id1_1_0_,
  13. userinfo0_.create_time as create_t2_1_0_,
  14. userinfo0_.create_user_id as create_u3_1_0_,
  15. userinfo0_.last_modified_time as last_mod4_1_0_,
  16. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  17. userinfo0_.version as version6_1_0_,
  18. userinfo0_.ages as ages7_1_0_,
  19. userinfo0_.email_address as email_ad8_1_0_,
  20. userinfo0_.last_name as last_nam9_1_0_,
  21. userinfo0_.name as name10_1_0_,
  22. userinfo0_.telephone as telepho11_1_0_,
  23. addresslis1_.user_info_id as user_inf8_0_1_,
  24. addresslis1_.id as id1_0_1_,
  25. addresslis1_.id as id1_0_2_,
  26. addresslis1_.create_time as create_t2_0_2_,
  27. addresslis1_.create_user_id as create_u3_0_2_,
  28. addresslis1_.last_modified_time as last_mod4_0_2_,
  29. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  30. addresslis1_.version as version6_0_2_,
  31. addresslis1_.city as city7_0_2_,
  32. addresslis1_.user_info_id as user_inf8_0_2_
  33. from user_info userinfo0_
  34. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  35. where userinfo0_.id in (?, ?, ?)

从代码中可以看到,我们查询所有的 Address 信息也只产生了 2 条 SQL;而当我们查询 UserInfo 的时候,SQL 最后的查询条件也变成了 in (?,?,?),同样的道理这样也会提升不少性能。

而 hibernate.default_batch_fetch_size 的经验参考值,可以设置成 20、30、50、100 等,太高了也没有意义。一个请求执行一次,产生的 SQL 数量为 3-5 条基本上都算合理情况,这样通过设置 default_batch_fetch_size 就可以很好地避免大部分业务场景下的 N+1 条 SQL 的性能问题了。

此时你还需要注意一点就是,在实际工作中,一定要知道我们一次操作会产生多少 SQL,有没有预期之外的 SQL 参数,这是需要关注的重点,这种情况可以利用我们之前说过的如下配置来开启打印 SQL,请看代码。

复制代码
  1. ## 显示sql的执行日志,如果开了这个,show_sql就可以不用了,show_sql没有上下文,多线程情况下,分不清楚是谁打印的,所有我推荐如下配置项:
  2. logging.level.org.hibernate.SQL=debug

但是这种配置也有个缺陷,就是只能全局配置,没办法针对不通过的实体管理关系配置不同的 Fetch Size 的值。

而与之类似的 Hibernate 里面也提供了一个注解 @BatchSize 可以解决此问题。

@BatchSize 注解

@BatchSize 注解是 Hibernate 提供的用来解决查询关联关系的批量处理大小,默认无,可以配置在实体上,也可以配置在关联关系上面。此注解里面只有一个属性 size,用来指定关联关系 LAZY 或者是 EAGER 一次性取数据的大小。

我们还是将上面的例子中的 UserInfo 实体做一下改造,在里面增加两次 @BatchSize 注解,代码如下所示。

复制代码
  1. @Entity
  2. @Data
  3. @SuperBuilder
  4. @AllArgsConstructor
  5. @NoArgsConstructor
  6. @Table
  7. @ToString(exclude = "addressList")
  8. @BatchSize(size = 2)//实体类上加@BatchSize注解,用来设置当被关联关系的时候一次查询的大小,我们设置成2,方便演示Address关联UserInfo的时候的效果
  9. public class UserInfo extends BaseEntity {
  10. private String name;
  11. private String telephone;
  12. @OneToMany(mappedBy = "userInfo",cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)
  13. @BatchSize(size = 20)//关联关系的属性上加@BatchSize注解,用来设置当通过UserInfo加载Address的时候一次取数据的大小
  14. private List<Address> addressList;
  15. }

我们通过改造 UserInfo 实体,可以直接演示 @BatchSize 应用在实体类和属性字段上的效果,所以 Address 实体可以不做任何改变,hibernate.default_batch_fetch_size 还改成默认值 -1,我们再分别执行一下两个 findAll 方法,看一下效果。

第一种:查询所有 UserInfo,代码如下面这行所示。

复制代码
  1. userInfoRepository.findAll()

我们看一下 SQL 控制台。

复制代码
  1.  org.hibernate.SQL :
  2. select userinfo0_.id as id1_1_,
  3. userinfo0_.create_time as create_t2_1_,
  4. userinfo0_.create_user_id as create_u3_1_,
  5. userinfo0_.last_modified_time as last_mod4_1_,
  6. userinfo0_.last_modified_user_id as last_mod5_1_,
  7. userinfo0_.version as version6_1_,
  8. userinfo0_.ages as ages7_1_,
  9. userinfo0_.email_address as email_ad8_1_,
  10. userinfo0_.last_name as last_nam9_1_,
  11. userinfo0_.name as name10_1_,
  12. userinfo0_.telephone as telepho11_1_
  13. from user_info userinfo0_ org.hibernate.SQL :
  14. select addresslis0_.user_info_id as user_inf8_0_1_,
  15. addresslis0_.id as id1_0_1_,
  16. addresslis0_.id as id1_0_0_,
  17. addresslis0_.create_time as create_t2_0_0_,
  18. addresslis0_.create_user_id as create_u3_0_0_,
  19. addresslis0_.last_modified_time as last_mod4_0_0_,
  20. addresslis0_.last_modified_user_id as last_mod5_0_0_,
  21. addresslis0_.version as version6_0_0_,
  22. addresslis0_.city as city7_0_0_,
  23. addresslis0_.user_info_id as user_inf8_0_0_
  24. from address addresslis0_
  25. where addresslis0_.user_info_id in (?, ?, ?)

和刚才设置 hibernate.default_batch_fetch_size=20 的效果一模一样,所以我们可以利用 @BatchSize 这个注解针对不同的关联关系,配置不同的大小,从而提升 N+1 SQL 的性能。

第二种:查询一下所有 Address,如下面这行代码所示。

复制代码
  1. addressRepository.findAll();

我们看一下控制台的 SQL 情况,如下所示。

复制代码
  1. org.hibernate.SQL :
  2. select address0_.id as id1_0_,
  3. address0_.create_time as create_t2_0_,
  4. address0_.create_user_id as create_u3_0_,
  5. address0_.last_modified_time as last_mod4_0_,
  6. address0_.last_modified_user_id as last_mod5_0_,
  7. address0_.version as version6_0_,
  8. address0_.city as city7_0_,
  9. address0_.user_info_id as user_inf8_0_
  10. from address address0_
  11. org.hibernate.SQL :
  12. select userinfo0_.id as id1_1_0_,
  13. userinfo0_.create_time as create_t2_1_0_,
  14. userinfo0_.create_user_id as create_u3_1_0_,
  15. userinfo0_.last_modified_time as last_mod4_1_0_,
  16. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  17. userinfo0_.version as version6_1_0_,
  18. userinfo0_.ages as ages7_1_0_,
  19. userinfo0_.email_address as email_ad8_1_0_,
  20. userinfo0_.last_name as last_nam9_1_0_,
  21. userinfo0_.name as name10_1_0_,
  22. userinfo0_.telephone as telepho11_1_0_,
  23. addresslis1_.user_info_id as user_inf8_0_1_,
  24. addresslis1_.id as id1_0_1_,
  25. addresslis1_.id as id1_0_2_,
  26. addresslis1_.create_time as create_t2_0_2_,
  27. addresslis1_.create_user_id as create_u3_0_2_,
  28. addresslis1_.last_modified_time as last_mod4_0_2_,
  29. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  30. addresslis1_.version as version6_0_2_,
  31. addresslis1_.city as city7_0_2_,
  32. addresslis1_.user_info_id as user_inf8_0_2_
  33. from user_info userinfo0_
  34. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  35. where userinfo0_.id in (?, ?)
  36. org.hibernate.SQL :
  37. select userinfo0_.id as id1_1_0_,
  38. userinfo0_.create_time as create_t2_1_0_,
  39. userinfo0_.create_user_id as create_u3_1_0_,
  40. userinfo0_.last_modified_time as last_mod4_1_0_,
  41. userinfo0_.last_modified_user_id as last_mod5_1_0_,
  42. userinfo0_.version as version6_1_0_,
  43. userinfo0_.ages as ages7_1_0_,
  44. userinfo0_.email_address as email_ad8_1_0_,
  45. userinfo0_.last_name as last_nam9_1_0_,
  46. userinfo0_.name as name10_1_0_,
  47. userinfo0_.telephone as telepho11_1_0_,
  48. addresslis1_.user_info_id as user_inf8_0_1_,
  49. addresslis1_.id as id1_0_1_,
  50. addresslis1_.id as id1_0_2_,
  51. addresslis1_.create_time as create_t2_0_2_,
  52. addresslis1_.create_user_id as create_u3_0_2_,
  53. addresslis1_.last_modified_time as last_mod4_0_2_,
  54. addresslis1_.last_modified_user_id as last_mod5_0_2_,
  55. addresslis1_.version as version6_0_2_,
  56. addresslis1_.city as city7_0_2_,
  57. addresslis1_.user_info_id as user_inf8_0_2_
  58. from user_info userinfo0_
  59. left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
  60. where userinfo0_.id = ?

这里可以看到,由于我们在 UserInfo 的实体上设置了 @BatchSize(size = 2),表示所有关联关系到 UserInfo 的时候一次取两条数据,所以就会发现这次我查询 Address 加载 UserInfo 的时候,产生了 3 条 SQL。

其中通过关联关系查询 UserInfo 产生了 2 条 SQL,由于我们 UserInfo 在数据库里面有三条数据,所以第一条 UserInfo 的 SQL 受 @BatchSize(size = 2) 控制,从而 in (?,?) 只支持了两个参数,同时也产生了第二条查 UserInfo 的 SQL。

从上面的例子中我们可以看到 @BatchSize 和 hibernate.default_batch_fetch_size 的效果是一样的,只不过一个是全局配置、一个是局部设置,这是可以减少 N+1 SQL 最直接、最方便的两种方式。

注意事项:

@BatchSize 的使用具有局限性,不能作用于 @ManyToOne 和 @OneToOne 的关联关系上,那样代码是不起作用的,如下所示。

复制代码
  1. public class Address extends BaseEntity {
  2. private String city;
  3. @ManyToOne(cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)
  4. @BatchSize(size = 30) //由于是@ManyToOne的关联关系所有没有作用
  5. private UserInfo userInfo;
  6. }

因此,你要注意 @BatchSize 只能作用在 @ManyToMany、@OneToMany、实体类这三个地方。
此外,Hibernate 中还提供了一种 FetchMode 的策略,包含三种模式,分别为 FetchMode.SELECT、FetchMode.JOIN,以及 FetchMode.Subselect。由于内容较多,我怕你一次性不好消化,所以会在下一讲继续为你介绍。到时见。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa

(0)

相关推荐