學習Spring-Data-Jpa(十一)---抓取策略與實體圖


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM