在 JPA 的使用過(guò)程中,N+1 SQL 是很常見(jiàn)的問(wèn)題,相信很多程序員都遇到過(guò)這一問(wèn)題,我看見(jiàn)很多同事處理起來(lái)束手無(wú)策,那么它究竟真的有那么麻煩嗎?這一講我會(huì )幫助你梳理思路,看看到底如何解決這個(gè)經(jīng)典問(wèn)題。
注:由于內容較多,我將這部分內容拆分成兩講,方便你學(xué)習。
想要解決一個(gè)問(wèn)題,必須要知道它是什么、如何產(chǎn)生的,這樣才能有方法、有邏輯地去解決它。下面通過(guò)一個(gè)例子來(lái)看一下什么是 N+1 的 SQL 問(wèn)題。
假設一個(gè) UserInfo 實(shí)體對象和 Address 是一對多的關(guān)系,即一個(gè)用戶(hù)有多個(gè)地址,我們首先看一下一般實(shí)體里面的關(guān)聯(lián)關(guān)系會(huì )怎么寫(xiě)。兩個(gè)實(shí)體對象如下述代碼所示。
- //UserInfo實(shí)體對象如下:
- @Entity
- @Data
- @SuperBuilder
- @AllArgsConstructor
- @NoArgsConstructor
- @Table
- @ToString(exclude = "addressList")//exclued防止 toString打印日志的時(shí)候死循環(huán)
- public class UserInfo extends BaseEntity {
- private String name;
- private String telephone;
- // UserInfo實(shí)體對象的關(guān)聯(lián)關(guān)系由Address對象里面的userInfo字段維護,默認是lazy加載模式,為了方便演示fetch取EAGER模式。此處是一對多關(guān)聯(lián)關(guān)系
- @OneToMany(mappedBy = "userInfo",fetch = FetchType.EAGER)
- private List<Address> addressList;
- }
- //Address對象如下:
- @Entity
- @Table
- @Data
- @SuperBuilder
- @AllArgsConstructor
- @NoArgsConstructor
- @ToString(exclude = "userInfo")
- public class Address extends BaseEntity {
- private String city;
- //維護UserInfo和Address的外鍵關(guān)系,方便演示也采用EAGER模式;
- @ManyToOne(fetch = FetchType.EAGER)
- @JsonBackReference //此注解防止JSON死循環(huán)
- private UserInfo userInfo;
- }
其次,我們假設數據庫里面有三條 UserInfo 的數據,ID 分別為 3、6、9,如下圖所示。

其中,每個(gè) UserInfo 分別有兩條 Address 數據,也就是一共 6 條 Address 的數據,如下圖所示。

然后,我們請求通過(guò) UserInfoRepository 查詢(xún)所有的 UserInfo 信息,方法如下面這行代碼所示。
- userInfoRepository.findAll()
現在,我們的控制臺將會(huì )得到四個(gè) SQL,如下所示。
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_,
- userinfo0_.create_time as create_t2_1_,
- userinfo0_.create_user_id as create_u3_1_,
- userinfo0_.last_modified_time as last_mod4_1_,
- userinfo0_.last_modified_user_id as last_mod5_1_,
- userinfo0_.version as version6_1_,
- userinfo0_.ages as ages7_1_,
- userinfo0_.email_address as email_ad8_1_,
- userinfo0_.last_name as last_nam9_1_,
- userinfo0_.name as name10_1_,
- userinfo0_.telephone as telepho11_1_
- from user_info userinfo0_ org.hibernate.SQL :
- select addresslis0_.user_info_id as user_inf8_0_0_,
- addresslis0_.id as id1_0_0_,
- addresslis0_.id as id1_0_1_,
- addresslis0_.create_time as create_t2_0_1_,
- addresslis0_.create_user_id as create_u3_0_1_,
- addresslis0_.last_modified_time as last_mod4_0_1_,
- addresslis0_.last_modified_user_id as last_mod5_0_1_,
- addresslis0_.version as version6_0_1_,
- addresslis0_.city as city7_0_1_,
- addresslis0_.user_info_id as user_inf8_0_1_
- from address addresslis0_
- where addresslis0_.user_info_id = ? org.hibernate.SQL :
- select addresslis0_.user_info_id as user_inf8_0_0_,
- addresslis0_.id as id1_0_0_,
- addresslis0_.id as id1_0_1_,
- addresslis0_.create_time as create_t2_0_1_,
- addresslis0_.create_user_id as create_u3_0_1_,
- addresslis0_.last_modified_time as last_mod4_0_1_,
- addresslis0_.last_modified_user_id as last_mod5_0_1_,
- addresslis0_.version as version6_0_1_,
- addresslis0_.city as city7_0_1_,
- addresslis0_.user_info_id as user_inf8_0_1_
- from address addresslis0_
- where addresslis0_.user_info_id = ? org.hibernate.SQL :
- select addresslis0_.user_info_id as user_inf8_0_0_,
- addresslis0_.id as id1_0_0_,
- addresslis0_.id as id1_0_1_,
- addresslis0_.create_time as create_t2_0_1_,
- addresslis0_.create_user_id as create_u3_0_1_,
- addresslis0_.last_modified_time as last_mod4_0_1_,
- addresslis0_.last_modified_user_id as last_mod5_0_1_,
- addresslis0_.version as version6_0_1_,
- addresslis0_.city as city7_0_1_,
- addresslis0_.user_info_id as user_inf8_0_1_
- from address addresslis0_
- where addresslis0_.user_info_id = ?
通過(guò) SQL 我們可以看得出來(lái),當取 UserInfo 的時(shí)候,有多少條 UserInfo 數據就會(huì )觸發(fā)多少條查詢(xún) Address 的 SQL。
那么所謂的 N+1 的 SQL,此時(shí) 1 代表的是一條 SQL 查詢(xún) UserInfo 信息;N 條 SQL 查詢(xún) Address 的信息。你可以想象一下,如果有 100 條 UserInfo 信息,可能會(huì )觸發(fā) 100 條查詢(xún) Address 的 SQL,性能多差呀。
很簡(jiǎn)單,這就是我們常說(shuō)的 N+1 SQL 問(wèn)題。我們這里使用的是 EAGER 模式,當使用 LAZY 的時(shí)候也是一樣的道理,只是生成 N 條 SQL 的時(shí)機是不一樣的。
上面我演示了 @OneToMany 的情況,那么我們再看一下 @ManyToOne 的情況。利用 AddressRepository 查詢(xún)所有的 Address 信息,方法如下面這行代碼所示。
- addressRepository.findAll();
這個(gè)時(shí)候我們再看一下控制臺,會(huì )產(chǎn)生如下 SQL。
- org.hibernate.SQL :
- select address0_.id as id1_0_,
- address0_.create_time as create_t2_0_,
- address0_.create_user_id as create_u3_0_,
- address0_.last_modified_time as last_mod4_0_,
- address0_.last_modified_user_id as last_mod5_0_,
- address0_.version as version6_0_,
- address0_.city as city7_0_,
- address0_.user_info_id as user_inf8_0_
- from address address0_
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id = ?
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id = ?
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id = ?
這里通過(guò) SQL 我們可以看得出來(lái),當取 Address 的時(shí)候,Address 里面有多少個(gè) user_info_id,就會(huì )觸發(fā)多少條查詢(xún) UserInfo 的 SQL。
那么所謂的 N+1 的 SQL,此時(shí) 1 就代表一條 SQL 查詢(xún) Address 信息;N 條 SQL 查詢(xún) UserInfo 的信息。同樣,你可以想象一下,如果我們有 100 條 Address 信息,分別有不同的 user_info_id 可能會(huì )觸發(fā) 100 條查詢(xún) UserInfo 的 SQL,性能依然很差。
這也是我們常說(shuō)的 N+1 SQL 問(wèn)題,我只是給你演示了 @OneToMany 和 @ManyToOne 的情況,@ManyToMany 和 @OneToOne 也是一樣的道理,都是當我們查詢(xún)主體信息時(shí)候,1 條 SQL 會(huì )衍生出來(lái)關(guān)聯(lián)關(guān)系的 N 條 SQL。
現在你認識了這個(gè)問(wèn)題,下一步該思考,怎么解決才更合理呢?有沒(méi)有什么辦法可以減少 SQL 條數呢?
最容易想到,就是有沒(méi)有什么機制可以減少 N 對應的 SQL 條數呢?從原理分析會(huì )知道,不管是 LAZY 還是 EAGER 都是沒(méi)有用的,因為這兩個(gè)只是決定了 N 條 SQL 的觸發(fā)時(shí)機,而不能減少 SQL 的條數。
不知道你是否還記得在第 20 講(Spring JPA 中的 Hibernate 加載過(guò)程與配置項是怎么回事)中,我們介紹過(guò)的 Hibernate 的配置項有哪些,如果你回過(guò)頭去看,會(huì )發(fā)現有個(gè)配置可以改變每次批量取數據的大小。
hibernate.default_batch_fetch_size 配置在 AvailableSettings.class 里面,指的是批量獲取數據的大小,默認是 -1,表示默認沒(méi)有匹配取數據。那么我們把這個(gè)值改成 20 看一下效果,只需要在 application.properties 里面增加如下配置即可。
- # 更改批量取數據的大小為20
- spring.jpa.properties.hibernate.default_batch_fetch_size= 20
在實(shí)體類(lèi)不發(fā)生任何改變的前提下,我們再執行如下兩個(gè)方法,分別看一下 SQL 的生成情況。
- userInfoRepository.findAll();
還是先查詢(xún)所有的 UserInfo 信息,看一下 SQL 的執行情況,代碼如下所示。
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_,
- userinfo0_.create_time as create_t2_1_,
- userinfo0_.create_user_id as create_u3_1_,
- userinfo0_.last_modified_time as last_mod4_1_,
- userinfo0_.last_modified_user_id as last_mod5_1_,
- userinfo0_.version as version6_1_,
- userinfo0_.ages as ages7_1_,
- userinfo0_.email_address as email_ad8_1_,
- userinfo0_.last_name as last_nam9_1_,
- userinfo0_.name as name10_1_,
- userinfo0_.telephone as telepho11_1_
- from user_info userinfo0_ org.hibernate.SQL :
- select addresslis0_.user_info_id as user_inf8_0_1_,
- addresslis0_.id as id1_0_1_,
- addresslis0_.id as id1_0_0_,
- addresslis0_.create_time as create_t2_0_0_,
- addresslis0_.create_user_id as create_u3_0_0_,
- addresslis0_.last_modified_time as last_mod4_0_0_,
- addresslis0_.last_modified_user_id as last_mod5_0_0_,
- addresslis0_.version as version6_0_0_,
- addresslis0_.city as city7_0_0_,
- addresslis0_.user_info_id as user_inf8_0_0_
- from address addresslis0_
- where addresslis0_.user_info_id in (?, ?, ?)
我們可以看到 SQL 直接減少到兩條了,其中查詢(xún) Address 的地方查詢(xún)條件變成了 in(?,?,?)。
想象一下,如果我們有 20 條 UserInfo 信息,那么產(chǎn)生的 SQL 也是兩條,此時(shí)要比 20+1 條 SQL 性能高太多了。
接著(zhù)我們再執行另一個(gè)方法,看一下 @ManyToOne 的情況,代碼如下所示。
- addressRepository.findAll()
關(guān)于執行的 SQL 情況如下所示。
- 2020-11-29 23:11:27.381 DEBUG 30870 --- [nio-8087-exec-5] org.hibernate.SQL :
- select address0_.id as id1_0_,
- address0_.create_time as create_t2_0_,
- address0_.create_user_id as create_u3_0_,
- address0_.last_modified_time as last_mod4_0_,
- address0_.last_modified_user_id as last_mod5_0_,
- address0_.version as version6_0_,
- address0_.city as city7_0_,
- address0_.user_info_id as user_inf8_0_
- from address address0_
- 2020-11-29 23:11:27.383 DEBUG 30870 --- [nio-8087-exec-5] org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id in (?, ?, ?)
從代碼中可以看到,我們查詢(xún)所有的 Address 信息也只產(chǎn)生了 2 條 SQL;而當我們查詢(xún) UserInfo 的時(shí)候,SQL 最后的查詢(xún)條件也變成了 in (?,?,?),同樣的道理這樣也會(huì )提升不少性能。
而 hibernate.default_batch_fetch_size 的經(jīng)驗參考值,可以設置成 20、30、50、100 等,太高了也沒(méi)有意義。一個(gè)請求執行一次,產(chǎn)生的 SQL 數量為 3-5 條基本上都算合理情況,這樣通過(guò)設置 default_batch_fetch_size 就可以很好地避免大部分業(yè)務(wù)場(chǎng)景下的 N+1 條 SQL 的性能問(wèn)題了。
此時(shí)你還需要注意一點(diǎn)就是,在實(shí)際工作中,一定要知道我們一次操作會(huì )產(chǎn)生多少 SQL,有沒(méi)有預期之外的 SQL 參數,這是需要關(guān)注的重點(diǎn),這種情況可以利用我們之前說(shuō)過(guò)的如下配置來(lái)開(kāi)啟打印 SQL,請看代碼。
- ## 顯示sql的執行日志,如果開(kāi)了這個(gè),show_sql就可以不用了,show_sql沒(méi)有上下文,多線(xiàn)程情況下,分不清楚是誰(shuí)打印的,所有我推薦如下配置項:
- logging.level.org.hibernate.SQL=debug
但是這種配置也有個(gè)缺陷,就是只能全局配置,沒(méi)辦法針對不通過(guò)的實(shí)體管理關(guān)系配置不同的 Fetch Size 的值。
而與之類(lèi)似的 Hibernate 里面也提供了一個(gè)注解 @BatchSize 可以解決此問(wèn)題。
@BatchSize 注解是 Hibernate 提供的用來(lái)解決查詢(xún)關(guān)聯(lián)關(guān)系的批量處理大小,默認無(wú),可以配置在實(shí)體上,也可以配置在關(guān)聯(lián)關(guān)系上面。此注解里面只有一個(gè)屬性 size,用來(lái)指定關(guān)聯(lián)關(guān)系 LAZY 或者是 EAGER 一次性取數據的大小。
我們還是將上面的例子中的 UserInfo 實(shí)體做一下改造,在里面增加兩次 @BatchSize 注解,代碼如下所示。
- @Entity
- @Data
- @SuperBuilder
- @AllArgsConstructor
- @NoArgsConstructor
- @Table
- @ToString(exclude = "addressList")
- @BatchSize(size = 2)//實(shí)體類(lèi)上加@BatchSize注解,用來(lái)設置當被關(guān)聯(lián)關(guān)系的時(shí)候一次查詢(xún)的大小,我們設置成2,方便演示Address關(guān)聯(lián)UserInfo的時(shí)候的效果
- public class UserInfo extends BaseEntity {
- private String name;
- private String telephone;
- @OneToMany(mappedBy = "userInfo",cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)
- @BatchSize(size = 20)//關(guān)聯(lián)關(guān)系的屬性上加@BatchSize注解,用來(lái)設置當通過(guò)UserInfo加載Address的時(shí)候一次取數據的大小
- private List<Address> addressList;
- }
我們通過(guò)改造 UserInfo 實(shí)體,可以直接演示 @BatchSize 應用在實(shí)體類(lèi)和屬性字段上的效果,所以 Address 實(shí)體可以不做任何改變,hibernate.default_batch_fetch_size 還改成默認值 -1,我們再分別執行一下兩個(gè) findAll 方法,看一下效果。
第一種:查詢(xún)所有 UserInfo,代碼如下面這行所示。
- userInfoRepository.findAll()
我們看一下 SQL 控制臺。
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_,
- userinfo0_.create_time as create_t2_1_,
- userinfo0_.create_user_id as create_u3_1_,
- userinfo0_.last_modified_time as last_mod4_1_,
- userinfo0_.last_modified_user_id as last_mod5_1_,
- userinfo0_.version as version6_1_,
- userinfo0_.ages as ages7_1_,
- userinfo0_.email_address as email_ad8_1_,
- userinfo0_.last_name as last_nam9_1_,
- userinfo0_.name as name10_1_,
- userinfo0_.telephone as telepho11_1_
- from user_info userinfo0_ org.hibernate.SQL :
- select addresslis0_.user_info_id as user_inf8_0_1_,
- addresslis0_.id as id1_0_1_,
- addresslis0_.id as id1_0_0_,
- addresslis0_.create_time as create_t2_0_0_,
- addresslis0_.create_user_id as create_u3_0_0_,
- addresslis0_.last_modified_time as last_mod4_0_0_,
- addresslis0_.last_modified_user_id as last_mod5_0_0_,
- addresslis0_.version as version6_0_0_,
- addresslis0_.city as city7_0_0_,
- addresslis0_.user_info_id as user_inf8_0_0_
- from address addresslis0_
- where addresslis0_.user_info_id in (?, ?, ?)
和剛才設置 hibernate.default_batch_fetch_size=20 的效果一模一樣,所以我們可以利用 @BatchSize 這個(gè)注解針對不同的關(guān)聯(lián)關(guān)系,配置不同的大小,從而提升 N+1 SQL 的性能。
第二種:查詢(xún)一下所有 Address,如下面這行代碼所示。
- addressRepository.findAll();
我們看一下控制臺的 SQL 情況,如下所示。
- org.hibernate.SQL :
- select address0_.id as id1_0_,
- address0_.create_time as create_t2_0_,
- address0_.create_user_id as create_u3_0_,
- address0_.last_modified_time as last_mod4_0_,
- address0_.last_modified_user_id as last_mod5_0_,
- address0_.version as version6_0_,
- address0_.city as city7_0_,
- address0_.user_info_id as user_inf8_0_
- from address address0_
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id in (?, ?)
- org.hibernate.SQL :
- select userinfo0_.id as id1_1_0_,
- userinfo0_.create_time as create_t2_1_0_,
- userinfo0_.create_user_id as create_u3_1_0_,
- userinfo0_.last_modified_time as last_mod4_1_0_,
- userinfo0_.last_modified_user_id as last_mod5_1_0_,
- userinfo0_.version as version6_1_0_,
- userinfo0_.ages as ages7_1_0_,
- userinfo0_.email_address as email_ad8_1_0_,
- userinfo0_.last_name as last_nam9_1_0_,
- userinfo0_.name as name10_1_0_,
- userinfo0_.telephone as telepho11_1_0_,
- addresslis1_.user_info_id as user_inf8_0_1_,
- addresslis1_.id as id1_0_1_,
- addresslis1_.id as id1_0_2_,
- addresslis1_.create_time as create_t2_0_2_,
- addresslis1_.create_user_id as create_u3_0_2_,
- addresslis1_.last_modified_time as last_mod4_0_2_,
- addresslis1_.last_modified_user_id as last_mod5_0_2_,
- addresslis1_.version as version6_0_2_,
- addresslis1_.city as city7_0_2_,
- addresslis1_.user_info_id as user_inf8_0_2_
- from user_info userinfo0_
- left outer join address addresslis1_ on userinfo0_.id = addresslis1_.user_info_id
- where userinfo0_.id = ?
這里可以看到,由于我們在 UserInfo 的實(shí)體上設置了 @BatchSize(size = 2),表示所有關(guān)聯(lián)關(guān)系到 UserInfo 的時(shí)候一次取兩條數據,所以就會(huì )發(fā)現這次我查詢(xún) Address 加載 UserInfo 的時(shí)候,產(chǎn)生了 3 條 SQL。
其中通過(guò)關(guān)聯(lián)關(guān)系查詢(xún) UserInfo 產(chǎn)生了 2 條 SQL,由于我們 UserInfo 在數據庫里面有三條數據,所以第一條 UserInfo 的 SQL 受 @BatchSize(size = 2) 控制,從而 in (?,?) 只支持了兩個(gè)參數,同時(shí)也產(chǎn)生了第二條查 UserInfo 的 SQL。
從上面的例子中我們可以看到 @BatchSize 和 hibernate.default_batch_fetch_size 的效果是一樣的,只不過(guò)一個(gè)是全局配置、一個(gè)是局部設置,這是可以減少 N+1 SQL 最直接、最方便的兩種方式。
注意事項:
@BatchSize 的使用具有局限性,不能作用于 @ManyToOne 和 @OneToOne 的關(guān)聯(lián)關(guān)系上,那樣代碼是不起作用的,如下所示。
- public class Address extends BaseEntity {
- private String city;
- @ManyToOne(cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)
- @BatchSize(size = 30) //由于是@ManyToOne的關(guān)聯(lián)關(guān)系所有沒(méi)有作用
- private UserInfo userInfo;
- }
因此,你要注意 @BatchSize 只能作用在 @ManyToMany、@OneToMany、實(shí)體類(lèi)這三個(gè)地方。
此外,Hibernate 中還提供了一種 FetchMode 的策略,包含三種模式,分別為 FetchMode.SELECT、FetchMode.JOIN,以及 FetchMode.Subselect。由于內容較多,我怕你一次性不好消化,所以會(huì )在下一講繼續為你介紹。到時(shí)見(jiàn)。
點(diǎn)擊下方鏈接查看源碼(不定時(shí)更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa
聯(lián)系客服