JPA/Hibernate技巧:获取子类定义的关联的最佳方法 - thorben


EntityGraphs和JOIN FETCH子句提供了一种简单有效的方法来获取实体并初始化其关联。但是,如果尝试将其与使用继承的域模型一起使用,则会很快遇到问题:
您不能在多态查询中使用此方法来获取在子类上定义的关联。换句话说,您的JOIN FETCH子句或EntityGraph需要引用由超类定义的实体属性。否则,Hibernate将抛出异常,因为某些子类的属性未知。
但是,有一个基于Hibernate的一级缓存的简单解决方法,它可以确保Hibernate Session中的每个数据库记录只有一个实体对象。让我们看一个例子,我将向您展示该解决方法的工作原理。

模型:

Publication有两个子类:BlogPost和Book。Publication还聚合关联了Author。

存储时,可使用InheritanceType.SINGLE_TABLE将Publication,Book和BlogPost实体映射到同一数据库表:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Publication {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    protected Long id;
     
    protected String title; 
     
    @Version
    private int version;
     
    @ManyToOne(fetch = FetchType.LAZY)
    protected Author author;
     
    protected LocalDate publishingDate;
 
    ...
}

@Entity
@DiscriminatorValue("Blog")
public class BlogPost extends Publication {
 
    private String url;
 
    ...
}

Book实体还定义了与Publisher实体的一对多关联。(疑问:根据类图,应该是BlogPost关联了Publisher实体,可能是类图画错啦)

@Entity
@DiscriminatorValue("Book")
public class Book extends Publication {
 
    private int pages;
 
    @ManyToOne
    private Publisher publisher;
 
    ...
}

InheritanceType.SINGLE_TABLE使我们能够定义一个多态一个一对多的关联之间的映射Author 和Publication 。

@Entity
public class Author {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
     
    @Version
    private int version;
 
    private String firstName;
 
    private String lastName;
 
    @OneToMany(mappedBy="author")
    private Set<Publication> publications = new HashSet<Publication>();
 
    ...
}

抓取带有BlogPosts, Books 和Publishers的Authors
如何初始化Book和Publisher实体之间的关联?如果您希望在1个查询中执行此操作,那么我会让您感到失望。Hibernate不支持。但是使用以下解决方法,您只需要2个查询。这比没有它所需的n + 1个查询要好得多。
那么它是怎样工作的?就像我说的那样,Hibernate仅在由多态关联的所有实体类定义的属性上支持JOIN FETCH子句或EntityGraphs。因此,您需要一个额外的查询才能将Book和其Publisher一起获得。在下一步中,您需要在处理第二个查询的结果时重用这些对象。

Hibernate的一级缓存
通过使用Hibernate的1级缓存,并保证在Hibernate Session中,数据库记录仅由1个实体对象映射,您可以非常高效地实现这一点。第一个查询获取用例所需的所有Book实体及其Publisher。使用JOIN FETCH子句来初始化Book与Publisher之间的关联。
第一句:

Query q1 = em.createQuery("SELECT b FROM Book b JOIN b.author a JOIN FETCH b.publisher p WHERE a.firstName = :fName");

当Hibernate处理此查询的结果时,它将所有Book实体对象添加到其一级缓存中。然后,当它需要处理另一个返回Book实体的查询的结果时,Hibernate首先检查这个Book实体对象是否已经存储在一级缓存中。如果是这样,那就从那里得到它。
这是此替代方法的关键要素。它使您可以在第二个查询中忽略Book和Publisher实体之间的关联。因为Hibernate将从一级缓存中获取所有Book实体对象,所以无论如何都将初始化与Publisher实体的关联。
第二句:

Query q2 = em.createQuery("SELECT p FROM Publication p JOIN p.author a WHERE a.firstName = :fName", Publication.class);

测试代码:

Query q1 = em.createQuery("SELECT b FROM Book b JOIN b.author a JOIN FETCH b.publisher p WHERE a.firstName = :fName");
q1.setParameter(
"fName", "Thorben");
List<Book> bs = q1.getResultList();
for (Book b : bs) {
    log.info(b);
}
 
Query q2 = em.createQuery(
"SELECT p FROM Publication p JOIN p.author a WHERE a.firstName = :fName", Publication.class);
q2.setParameter(
"fName", "Thorben");
List<Publication> ps = q2.getResultList();
 
for (Publication p : ps) {
    if (p instanceof BlogPost) {
        BlogPost blog = (BlogPost) p;
        log.info(
"BlogPost - "+blog.getTitle()+" was published at "+blog.getUrl());
    } else {
        Book book = (Book) p;
        log.info(
"Book - "+book.getTitle()+" was published by "+book.getPublisher().getName());
        log.info(book);
    }
}

正如您在日志文件中看到的那样,Hibernate仅执行了两个预期查询。即使第二个查询未初始化Book与Publisher之间的关联,也可以使用延迟获取的关联。如记录的对象引用所示,Hibernate 在两个查询的结果中使用了相同的Book实体对象。
12:18:05,504 DEBUG [org.hibernate.SQL] - select book0_.id as id2_1_0_, publisher2_.id as id1_2_1_, book0_.author_id as author_i8_1_0_, book0_.publishingDate as publishi3_1_0_, book0_.title as title4_1_0_, book0_.version as version5_1_0_, book0_.pages as pages6_1_0_, book0_.publisher_id as publishe9_1_0_, publisher2_.name as name2_2_1_ from Publication book0_ inner join Author author1_ on book0_.author_id=author1_.id inner join Publisher publisher2_ on book0_.publisher_id=publisher2_.id where book0_.DTYPE='Book' and author1_.firstName=?
12:18:05,537 INFO  [org.thoughts.on.java.TestJpaInheritance] - org.thoughts.on.java.model.Book@3458eca5
12:18:05,551 DEBUG [org.hibernate.SQL] - select publicatio0_.id as id2_1_, publicatio0_.author_id as author_i8_1_, publicatio0_.publishingDate as publishi3_1_, publicatio0_.title as title4_1_, publicatio0_.version as version5_1_, publicatio0_.pages as pages6_1_, publicatio0_.publisher_id as publishe9_1_, publicatio0_.url as url7_1_, publicatio0_.DTYPE as dtype1_1_ from Publication publicatio0_ inner join Author author1_ on publicatio0_.author_id=author1_.id where author1_.firstName=?
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - Book - Hibernate Tips - More than 70 solutions to common Hibernate problems was published by Myself
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - org.thoughts.on.java.model.Book@3458eca5
12:18:05,555 INFO  [org.thoughts.on.java.TestJpaInheritance] - BlogPost - Best way to fetch an association defined by a subclass was published at https://thorben-janssen.com/fetch-association-of-subclass/