學習Spring-Data-Jpa(十八)---JPA的繼承策略


  前面學習過@MappedSuperclass,可以把一些公共的屬性提取到添加該注解的類里,如id,creteTime,updateTime等。該類不會生成表,該類中相應的字段都會生成在子類中。該類沒有被@Entity注釋,不是一個實體。

@Inheritance

  JPA為我們提供了@Inheritance注解來控制實體時間有繼承關系時,在數據庫中如何生成對應的表。該注解需要添加在根實體上,如果沒有添加@Inheritance注解,或添加了沒有指定InheritanceType那么將使用SINGLE_TABLE(單表)映射策略。
  下面我們通過例子來看一下三個繼承策略有什么不同,如書籍(Book)下有兩個子類,紙質書(PrintBook)、電子書(EBook)。

SINGLE_TABLE(單表映射策略)

  不添加@Inheritance或InheritanceType.SINGLE_TABLE(默認的策略)都會使用該策略。

  Book實體

/**
 * 書籍實體
 * @author caofanqi
 */
@Data
@Entity
@Builder
@Table(name = "jpa_book")
//@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NoArgsConstructor
@AllArgsConstructor
@NamedEntityGraphs({
    @NamedEntityGraph(name = "Book.fetch.category",attributeNodes = {@NamedAttributeNode("category")})
})
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private  String bookName;

    private LocalDate publishDate;

    /**
     * 書和門類是多對一的關系
     */
    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "category_id",referencedColumnName = "id")
    private Category category;

    /**
     * 書和作者是多對多的關系,我們讓book為關聯擁有放,添加@JoinTable注解。
     */
    @JsonIgnore
    @ManyToMany
    @JoinTable(name = "jpa_book_author",
            joinColumns = @JoinColumn(name="book_id", referencedColumnName="id"),
            inverseJoinColumns= @JoinColumn(name="author_id", referencedColumnName="id"))
    private List<Author> authors;

}

  PrintBook實體

/**
 * 紙質書籍
 * @author caofanqi
 */
@Data
@Entity
@Table(name = "jpa_print_book")
public class PrintBook extends Book{

    /**
     * 印刷時間
     */
    private LocalDate printDate;

}

  EBook實體

/**
 *電子書籍
 * @author caofanqi
 */
@Data
@Entity
@Table(name = "jpa_ebook")
public class EBook extends Book{

    /**
     * 格式
     */
    private String format;

}

  測試用例1:

    @Test
    void testExtends1(){

        Book book = new Book();
        book.setBookName("書籍001");
        bookRepository.save(book);

        PrintBook printBook = new PrintBook();
        printBook.setBookName("紙質書籍001");
        printBook.setPrintDate(LocalDate.of(2019,12,20));
        bookRepository.save(printBook);

        EBook eBook = new EBook();
        eBook.setBookName("電子書001");
        eBook.setFormat("txt");
        bookRepository.save(eBook);

    }

  查看生成的表結構和打印的SQL語句如下:

  

  

  測試用例2:

    @Test
    void testExtends2(){

        List<Book> books = bookRepository.findAll();
        books.forEach(b -> System.out.println(b.getClass().getSimpleName()));

    }

  運行結果:

Hibernate: select book0_.id as id2_8_, book0_.book_name as book_nam3_8_, book0_.category_id as category7_8_, book0_.publish_date as publish_4_8_, book0_.format as format5_8_, book0_.print_date as print_da6_8_, book0_.dtype as dtype1_8_ from cfq_jpa_book book0_
Book
PrintBook
EBook

  為PrintBook建立單獨的Repository:

/**
 * @author caofanqi
 */
public interface PrintBookRepository extends JpaRepositoryImplementation<PrintBook,Long> {
}

  測試用例3:

    @Test
    void testExtends3(){
        List<EBook> books = eBookRepository.findAll();
        books.forEach(b -> System.out.println(b.getClass().getSimpleName()));
        EBook eBook = books.get(0);
        eBook.setBookName("EBook001");
        eBook.setFormat("pdf");
    }

  運行結果:

Hibernate: select ebook0_.id as id2_8_, ebook0_.book_name as book_nam3_8_, ebook0_.category_id as category7_8_, ebook0_.publish_date as publish_4_8_, ebook0_.format as format5_8_ from cfq_jpa_book ebook0_ where ebook0_.dtype='EBook'
EBook
Hibernate: update cfq_jpa_book set book_name=?, category_id=?, publish_date=?, format=? where id=?

 

  得出結論:

       1、使用SINGLE_TABLE策略時,子類不會生成對應的表,所以子類中的@Table注解多余了。而父類的表中會多出一個dtype字段,由JPA來維護,字段的值是類的簡單名稱,用來標識是屬於哪一類的。

       2、父類和子類之間差異不大,父類維護了大量的相同字段,子類只有少量字段不同,且子類字段可以為空的情況下,才能使用。

       3、可以為每個子類單獨創建Repository,進行持久層操作。

JOINED(關聯映射策略)

  特定於子類的字段被映射到一個單獨的表,而不是父類的公共字段,並執行聯接來實例化子類。

  我們將之前生成的book表刪除,並修改Book類@Inheritance的策略為InheritanceType.JOINED。

  運行測試用例1生成的表結構和打印的SQL如下:

  

       

  

  運行測試用例2,SQL打印及結果如下:

Hibernate: select book0_.id as id1_8_, book0_.book_name as book_nam2_8_, book0_.category_id as category4_8_, book0_.publish_date as publish_3_8_, book0_1_.format as format1_11_, book0_2_.print_date as print_da1_14_, case when book0_1_.id is not null then 1 when book0_2_.id is not null then 2 when book0_.id is not null then 0 end as clazz_ from cfq_jpa_book book0_ left outer join cfq_jpa_ebook book0_1_ on book0_.id=book0_1_.id left outer join cfq_jpa_print_book book0_2_ on book0_.id=book0_2_.id
EBook
PrintBook
Book

  運行測試用例3,SQL打印及結果如下:

Hibernate: select ebook0_.id as id1_8_, ebook0_1_.book_name as book_nam2_8_, ebook0_1_.category_id as category4_8_, ebook0_1_.publish_date as publish_3_8_, ebook0_.format as format1_11_ from cfq_jpa_ebook ebook0_ inner join cfq_jpa_book ebook0_1_ on ebook0_.id=ebook0_1_.id
EBook
Hibernate: update cfq_jpa_book set book_name=?, category_id=?, publish_date=? where id=?
Hibernate: update cfq_jpa_ebook set format=? where id=?

  得出結論:

    1、會生成多張表,在父類對應的表中存放了公共的字段,在子類對應的表中,存放了子類特有的字段,還有一個指向父類id的外鍵。

    2、在執行新增操作時,如果是子類新增,會執行兩條insert語句,分別插入到父類對應的表和自己對應的表中。

    3、在使用父類的Repository進行findAll查詢時,父類對應的表會左外連接所有子類所對應的表進行查詢,數據量大,且子類多時,發雜的SQL,會有性能問題。

    4、在使用子類自己的Repository進行findAll查詢時,子類對應的表會內連接類對對應的表。

TABLE_PER_CLASS(每個實體對應一個表)

  我們把之前生成的book、printbook、ebook表刪除,並修改Book類@Inheritance的策略為InheritanceType.TABLE_PER_CLASS。

  直接運行測試用例1會報如下錯誤:

org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass> mapping for: cn.caofanqi.study.studyspringdatajpa.pojo.domain.PrintBook

  意思是使用TABLE_PER_CLASS策略時,不能使用主鍵自增策略。因為每一個類都會生成一張表,當使用父類的Repository進行查詢時,會把這些表的數據都抓出來,然后每一個表里面都會有各自的主鍵,如果用自增策略的話,兩張表中的主鍵可能會一樣,比如說book表中有id為1的記錄,ebook表中也有id為1的記錄。這樣把他們聚合到一塊的時候,就會有兩個id為1的book對象。所以不能使用主鍵自增策略。

  修改Book實體如下:

/**
 * 書籍實體
 * @author caofanqi
 */
@Data
@Entity
@Builder
@Table(name = "jpa_book")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@NoArgsConstructor
@AllArgsConstructor
@NamedEntityGraphs({
    @NamedEntityGraph(name = "Book.fetch.category",attributeNodes = {@NamedAttributeNode("category")})
})
public class Book {

    @Id
    @GeneratedValue(generator = "sequenceGenerator") //指定生成器
    @GenericGenerator(name = "sequenceGenerator", strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",//為生成器指定策略(hibernate默認提供的)
            parameters = {
                    @org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "id_sequence"),//指定要用的序列或表的名稱
                    @org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "1000"),//sequence從幾開始
                    @org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "1"),//增量步長
                    @org.hibernate.annotations.Parameter(name = SequenceStyleGenerator.OPT_PARAM, value = "pooled"),//指定優化器,用於提高性能
            }
    )
    private Long id;

    private  String bookName;

    private LocalDate publishDate;

    /**
     * 書和門類是多對一的關系
     */
    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "category_id",referencedColumnName = "id")
    private Category category;

    /**
     * 書和作者是多對多的關系,我們讓book為關聯擁有放,添加@JoinTable注解。
     */
    @JsonIgnore
    @ManyToMany
    @JoinTable(name = "jpa_book_author",
            joinColumns = @JoinColumn(name="book_id", referencedColumnName="id"),
            inverseJoinColumns= @JoinColumn(name="author_id", referencedColumnName="id"))
    private List<Author> authors;

}

  運行測試用例1生成的表結構和打印的SQL如下:

   

      

  

  運行測試用例2,SQL打印及結果如下:

Hibernate: select book0_.id as id1_8_, book0_.book_name as book_nam2_8_, book0_.category_id as category4_8_, book0_.publish_date as publish_3_8_, book0_.format as format1_11_, book0_.print_date as print_da1_14_, book0_.clazz_ as clazz_ from ( select id, book_name, publish_date, category_id, null as format, null as print_date, 0 as clazz_ from cfq_jpa_book union all select id, book_name, publish_date, category_id, format, null as print_date, 1 as clazz_ from cfq_jpa_ebook union all select id, book_name, publish_date, category_id, null as format, print_date, 2 as clazz_ from cfq_jpa_print_book ) book0_
Book
EBook
PrintBook

  運行測試用例3,SQL打印及結果如下:

Hibernate: select ebook0_.id as id1_8_, ebook0_.book_name as book_nam2_8_, ebook0_.category_id as category4_8_, ebook0_.publish_date as publish_3_8_, ebook0_.format as format1_11_ from cfq_jpa_ebook ebook0_
EBook
Hibernate: update cfq_jpa_ebook set book_name=?, category_id=?, publish_date=?, format=? where id=?

  得出結論:

    1、會生成多張表,父類對應的表中中有自己的字段。子類對應的表中當中除了有自己特有的字段外,也有父類所有的字段。
    2、執行新增操作時,執行一次insert到自己對應的表。
    3、在使用父類的Repository進行findAll查詢時,會將所有的表進行UNION ALL操作。
    4、在使用子類自己的Repository進行findAll查詢時,單表SELECT。
    5、適用於父類和子類差以比較大的時候,如果繼承體系比較小時。

 

 源碼地址:https://github.com/caofanqi/study-spring-data-jpa


免責聲明!

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



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