1、抓取策略
在前面說到的關聯關系注解中,都有一個fetch屬性,@OneToOne、@ManyToOne中都默認是FetchType.EAGER,立即獲取。@OneToMany、@ManyToMany默認值是FetchType.LAZY,延遲獲取。這些注解的的fetch屬性定義的是合適獲取,至於如何獲取,對與FetchType.EAGER,使用的是JOIN。FetchType.LAZY使用的是SELECT。JPA並沒有提供我們設置如何獲取的方式,如果想要進行修改要使用Hibernate提供的Fetch注解配置FetchMode。里面提供了三種方式SELECT、JOIN、SUBSELECT。(大多數情況下,我們不需要進行設置如何加載,使用默認的即可)
但是對於JPA的fetch,使用起來只有在使用Spring-Data-Jpa為我們提供的findById方法時,配置的fetch=FetchType.EAGER才會生效。而我們根據Spring-Data-Jpa規則定義的方法查詢則不生效,還是會進行延遲加載。
1.1、執行findById會進行關聯查詢
/** * 對於fetch= FetchType.EAGER ,使用findById會執行關聯查詢。 */ @Test void testFindById(){ Optional<Book> bookOptional = bookRepository.findById(1L); if (bookOptional.isPresent()) { Book book = bookOptional.get(); System.out.println(book.getCategory().getCategoryName()); } }
findById控制台的打印信息
Hibernate: select book0_.id as id1_4_0_, book0_.book_name as book_nam2_4_0_, book0_.category_id as category4_4_0_, book0_.publish_date as publish_3_4_0_, category1_.id as id1_6_1_, category1_.category_name as category2_6_1_, category1_.parent_id as parent_i3_6_1_ from cfq_jpa_book book0_ left outer join cfq_jpa_category category1_ on book0_.category_id=category1_.id where book0_.id=? Java
1.2、執行findByBookName不會進行關聯查詢
/** * 根據書名進行查詢書籍 * @param bookName bookName * @return book */ Optional<Book> findByBookName(String bookName);
/** * 對於fetch= FetchType.EAGER ,使用我們自己定義的查詢方法,則不生效,會使用懶加載的方式 */ @Test void findByBookName(){ Optional<Book> bookOptional = bookRepository.findByBookName("java編程思想"); if (bookOptional.isPresent()) { Book book = bookOptional.get(); System.out.println(book.getCategory().getCategoryName()); } }
findByBookName控制台的打印信息
Hibernate: select book0_.id as id1_4_, book0_.book_name as book_nam2_4_, book0_.category_id as category4_4_, book0_.publish_date as publish_3_4_ from cfq_jpa_book book0_ where book0_.book_name=? Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=? Java
這樣的話,如果我們對於圖書(Book)來說,我們使用findById方法時,是可以直接拿到門類(Category)信息的。但是通過findByBookName進行查詢時,只有我們使用到門類的時候,才會發送一條查詢門類的SQL,只是對於一條記錄還好。但是如果我們查詢一個圖書列表(N本圖書)的時候,這時就會執行N+1條SQL。如下所示,根據出版時間進行查詢,一共有3條記錄,執行了4句SQL。
/** * 對於fetch= FetchType.EAGER ,使用我們自己定義的查詢方法,則不生效,會使用懶加載的方式,執行 N+1條SQL。 * */ @Test void findByPublishDate(){ List<Book> books = bookRepository.findByPublishDate(LocalDate.of(2019,11,17)); books.forEach(b -> System.out.println(b.getCategory().getCategoryName())); }
Hibernate: select book0_.id as id1_4_, book0_.book_name as book_nam2_4_, book0_.category_id as category4_4_, book0_.publish_date as publish_3_4_ from cfq_jpa_book book0_ where book0_.publish_date=? Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=? Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=? Hibernate: select category0_.id as id1_6_0_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_ from cfq_jpa_category category0_ where category0_.id=? Java 數據結構 數據庫
對於這個問題,我們怎么來解決呢?
2、使用@Query自己寫JPQL語句進行解決N+1條SQL問題。
/** * 使用@Query,JPQL中 聲明要查詢category屬性,減少子查詢。 * @param publishDate publishDate * @return list */ @Query(value = "select b,b.category from Book b where b.publishDate = :publishDate ") // @Query(value = "select b,c from Book b inner join Category c on b.category = c where b.publishDate = :publishDate ") List<Book> findByPublishDateWithQuery(LocalDate publishDate);
/** * 對於fetch= FetchType.EAGER ,使用@Query,自己寫查詢語句,解決N+1條SQL問題。 */ @Test void findByPublishDateWithQuery(){ List<Book> books = bookRepository.findByPublishDateWithQuery(LocalDate.of(2019, 11, 17)); books.forEach(b -> System.out.println(b.getCategory().getCategoryName())); }
findByPublishDateWithQuery控制台打印的信息
Hibernate: select book0_.id as id1_4_0_, category1_.id as id1_6_1_, book0_.book_name as book_nam2_4_0_, book0_.category_id as category4_4_0_, book0_.publish_date as publish_3_4_0_, category1_.category_name as category2_6_1_, category1_.parent_id as parent_i3_6_1_ from cfq_jpa_book book0_ inner join cfq_jpa_category category1_ on book0_.category_id=category1_.id where book0_.publish_date=? Java 數據結構 數據庫
在很多情況下,我們使用Spring-Data-Jpa,一些簡單的查詢,我們都喜歡用定義方法查詢,而不是寫JPQL。JPA為我們提供了一組注解:使用Spring-Data-Jpa為我們提供的@EntityGraph,或@EntityGraph和@NamedEntityGraph進行解決。
3、@NamedEntityGraphs、@NamedEntityGraph、@EntityGraph
3.1、@NamedEntityGraphs:用於對@NamedEntityGraph注解進行分組。
3.2、@NamedEntityGraph:用於指定查找操作或查詢的路徑和邊界。
屬性name:(可選) 實體圖的名稱。 默認為根實體的實體名。
屬性attributeNodes:(可選) 包含在該圖中的實體屬性列表。
屬性:includeAllAttributes:(可選)將注釋實體類的所有屬性作為屬性節點包含在NamedEntityGraph中,而無需顯式列出它們。包含的屬性仍然可以由引用子圖的屬性節點完全指定。默認為false。一般不需要設置。
屬性subgraphs:(可選)包含在實體圖中的子圖列表。這些是從NamedAttributeNode定義中按名稱引用的。
屬性subclassSubgraphs:(可選) 子圖列表 這些子圖將向實體圖添加注釋實體類的子類的附加屬性。超類中的指定屬性包含在子類中。
3.3、@EntityGraph: 注解用於配置 JPA 2.1規范支持的javax.persistence.EntityGraph,應該使用在repository的方法上面。從1.9開始,我們支持動態EntityGraph定義,允許通過attributePaths()配置自定義fetch-graph。如果指定了attributePaths(),則忽略entity-graph的name(也就是配置的value()),並將EntityGraph視為動態的。
屬性value:要使用的名稱。如果為空,則返回JpaQueryMethod.getNamedQueryName()作為value。一般為@NamedEntityGraph的name值,或者不填使用自己的attributePaths屬性。
屬性type:要使用的EntityGraphType,默認為EntityGraphType.FETCH。
屬性attributePaths:要使用的屬性路徑,默認為空。可以直接引用實體屬性,也可以通過roperty.nestedProperty引用嵌套屬性。
枚舉EntityGraphType:
LOAD("javax.persistence.loadgraph"):當javax.persistence.loadgraph屬性用於指定實體圖時,由實體圖的attributePaths指定的屬性將被視為FetchType.EAGER,未指定的屬性,將根據其設置的或默認的FetchType來進行處理。
FETCH("javax.persistence.fetchgraph"):當javax.persistence.fetchgraph屬性用於指定實體圖時,由實體圖的attributePaths指定的屬性將被視為FetchType.EAGER,而未指定的屬性被視為FetchType.LAZY。
3.4、使用方法1:
3.4.1、在實體上定義一個NamedEntityGraph
3.4.2、在Repository的查詢方法上引用實體圖。
3.4.3、測試根據出版時間進行查詢,由4條SQL變為3條。
3.5、使用方法2:也可以不用再實體上定義NamedEntityGraph,直接使用@EntityGraph的attributePaths屬性來設置,效果是一樣的。只不過如果有多個屬性都要一起查出來,而且有多個方法都用到了,使用@EntityGraph的attributePaths屬性修改起來就不是那么方便了,結合自己的情況進行選擇。
4、對於具有父子關系的處理
場景:門類(Category),常常具有父子關系,比如說,文學類圖書下面可能有小說分類,而小說分類下,又分為長、中、短篇小說。我們怎么一次查出需要的樹形結果呢?
准備工作:
4.1、Category實體:
/** * 類別 * @author caofanqi */ @Data @Entity @Builder @Table(name = "jpa_category") @NoArgsConstructor @AllArgsConstructor public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String categoryName; /** * 父門類,通過parent_id來維護父子關系。 * 使用@ToString.Exclude,解決lombok的toString方法循環引用問題。 */ @ToString.Exclude @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id",referencedColumnName = "id") private Category parent; /** * 子門類列表,交由parent來維護兩者之間關系。 */ @OneToMany(mappedBy = "parent",cascade = CascadeType.ALL) private List<Category> children; /** * 門類和書是一對多的關系 * 由多的一方來維護關聯關系 */ @OneToMany(mappedBy = "category") @OrderBy("bookName DESC") private List<Book> books; }
4.2、數據准備
4.3、對於數據量比較小,我們可以重寫JpaRepository的findAll方法,並添加@EntityGraph注解,抓取子節點,如下所示:
@Override @EntityGraph(attributePaths = "children") List<Category> findAll();
測試用例:
/** * 測試 一次查詢樹形結構 */ @Test void findAll(){ List<Category> categories = categoryRepository.findAll(); categories.stream().filter(c -> c.getParent() == null).forEach(c -> printName(c,null)); } private void printName(Category category,String prefix){ if (StringUtils.isEmpty(prefix)){ prefix = "---"; } System.out.println(prefix + category.getCategoryName()); List<Category> children = category.getChildren(); if (!CollectionUtils.isEmpty(children)){ for (Category c : children){ printName(c,prefix + "---"); } } }
控制台輸出信息:
Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id ---計算機科學圖書 ------Java ------數據庫 ------數據結構 ---文學圖書 ------小說類 ---------長篇小說 ---------中篇小說 ---------短篇小說
這種方式的優點是,不管層級多深,只有一次join。缺點是需要查詢出來全部的門類,然后再代碼中過濾出頂級門類,出給前端使用。而且,對於只查詢某一門類,和下面的子門類不適用。
4.4、根據父門類,一次性查詢子門類及子門類的所有子節點。
4.4.1、findByParent
/** * 查詢根據父節點查詢門類 * @return list */ @EntityGraph(attributePaths = {"children"}) List<Category> findByParent(Category category);
4.4.2、這時我們測試發現,只是第一層的門類不用再執行SQL了,而下面的門類一樣要執行。
@Test void findByParent(){ List<Category> categories = categoryRepository.findByParent(null); categories.forEach(c -> printName(c,null)); }
Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id where category0_.parent_id is null ---計算機科學圖書 ------Java Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ------數據庫 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ------數據結構 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ---文學圖書 ------小說類 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ---------長篇小說 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ---------中篇小說 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=? ---------短篇小說 Hibernate: select children0_.parent_id as parent_i3_6_0_, children0_.id as id1_6_0_, children0_.id as id1_6_1_, children0_.category_name as category2_6_1_, children0_.parent_id as parent_i3_6_1_ from cfq_jpa_category children0_ where children0_.parent_id=?
4.4.3、解決多次查詢問題,上面說到@EntityGraph的attributePaths是支持屬性嵌套的,我們寫一個children就會關聯一次,如果我們知道層級的話,可以用.進行連接children,如下圖,就會與自己關聯三次有幾層,就要至少有幾個children,也就會進行幾次關聯。(層級越多,關聯的次數越多)
也可以使用@NamedEntityGraph(感覺不如attributePaths簡介),寫法如下:
@NamedEntityGraph(name = "Category.findByParent", attributeNodes = {@NamedAttributeNode(value = "children", subgraph = "son")}, //第一層 subgraphs = {@NamedSubgraph(name = "son", attributeNodes = @NamedAttributeNode(value = "children", subgraph = "grandson")), //第二層 @NamedSubgraph(name = "grandson", attributeNodes = @NamedAttributeNode(value = "children"))//第三層 })
但是現在光做這些還不夠,執行測試用例,會拋出 org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:異常;想知道為啥的可以點擊這里。
我推薦兩個解決辦法:
①將List集合修改為Set,並使用@EqualsAndHashCode.Exclude解決lombok的hashcode方法引入的異常。
②使用@OrderColumn,這樣jpa會在數據庫中多出一列,用於自己維護關系。(一開始就要這樣哦,半路改的,會有問題)
以上任意一種修改后,執行測試用例,控制台輸出結果如下:
Hibernate: select category0_.id as id1_6_0_, children1_.id as id1_6_1_, children2_.id as id1_6_2_, children3_.id as id1_6_3_, category0_.category_name as category2_6_0_, category0_.parent_id as parent_i3_6_0_, children1_.category_name as category2_6_1_, children1_.parent_id as parent_i3_6_1_, children1_.parent_id as parent_i3_6_0__, children1_.id as id1_6_0__, children1_.children_order as children4_0__, children2_.category_name as category2_6_2_, children2_.parent_id as parent_i3_6_2_, children2_.parent_id as parent_i3_6_1__, children2_.id as id1_6_1__, children2_.children_order as children4_1__, children3_.category_name as category2_6_3_, children3_.parent_id as parent_i3_6_3_, children3_.parent_id as parent_i3_6_2__, children3_.id as id1_6_2__, children3_.children_order as children4_2__ from cfq_jpa_category category0_ left outer join cfq_jpa_category children1_ on category0_.id=children1_.parent_id left outer join cfq_jpa_category children2_ on children1_.id=children2_.parent_id left outer join cfq_jpa_category children3_ on children2_.id=children3_.parent_id where category0_.parent_id is null ---文學圖書 ------小說類 ---------長篇小說 ---------中篇小說 ---------短篇小說 ---計算機科學圖書 ------Java ------數據庫 ------數據結構
源碼地址:https://github.com/caofanqi/study-spring-data-jpa