The best way to fetch an association defined by a subclass
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
EntityGraphs and JOIN FETCH clauses provide an easy and efficient way to fetch an entity and initialize its associations. But if you try to use it with a domain model that uses inheritance, you will quickly run into an issue:
You can’t use this approach in a polymorphic query to fetch an association that’s defined on a subclass. Or in other words, your JOIN FETCH clause or EntityGraph needs to reference an entity attribute that’s defined by your superclass. Otherwise, Hibernate will throw an exception because the attribute is unknown for some subclasses.
But there is an easy workaround based on Hibernate’s 1st level cache and its guarantee that there is only 1 entity object for each database record in a Hibernate Session. Let’s take a look at an example, and I’ll show you how this workaround works.
Note: This article was inspired by a question on StackOverflow for which I was able to claim the bounty with an answer I prepared on a Coffee with Thorben live stream.
The domain model
The model used in this article is simple. An Author can write different kinds of Publications, like Books and BlogPosts. These 2 kinds of Publications share the attributes id, version, title, publishingDate, and a reference to the Author. BlogPosts get published on their author’s blog, so they have the additional attribute url. Books might be published by a Publisher, which I modeled as a reference to another entity in our small domain model.
There is nothing special about the entity mappings. I use the InheritanceType.SINGLE_TABLE to map the Publication, Book, and BlogPost entities to the same database table.
@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; ... }
The Book entity also defines a one-to-many association to the Publisher entity.
@Entity @DiscriminatorValue("Book") public class Book extends Publication { private int pages; @ManyToOne private Publisher publisher; ... }
The InheritanceType.SINGLE_TABLE enables us to define a polymorphic one-to-many association mapping between the Author and the Publication entity.
@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>(); ... }
Fetching an Author with their BlogPosts, Books and Publishers
OK, let’s answer our initial question: How can you initialize the association between the Book and the Publisher entity if you get an Author with all their Publication?
If you expected to do this in 1 query, I have to disappoint you. Hibernate doesn’t support that. But using the following workaround, you only need 2 queries. That is much better than the n+1 queries you would need without it.
So, how does it work? As I said, Hibernate only supports JOIN FETCH clauses or EntityGraphs on attributes that are defined by all entity classes of a polymorphic association. Due to that, you need an extra query to get the Books with their Publishers. In the next step, you then need to reuse these objects when processing the results of the 2nd query.
Hibernate’s 1st level cache to the rescue
By using Hibernate’s 1st level cache and its guarantee that within a Hibernate Session, a database record gets only mapped by 1 entity object, you can implement this very efficiently. Your 1st query gets all the Book entities and their Publisher, which you need for your use case.
In this example, these are all Books written by an Author with the firstName Thorben. As you can see, the query isn’t too complex. I join from the Book to the Author to be able to define the WHERE clause, and I use a JOIN FETCH clause to initialize the association between the Book and the Publisher.
Query q1 = em.createQuery("SELECT b FROM Book b JOIN b.author a JOIN FETCH b.publisher p WHERE a.firstName = :fName");
When Hibernate processes the result of this query, it adds all entity objects to its 1st level cache. When it then needs to process the result of another query which returns Book entities, Hibernate first checks if that entity object is already stored in the 1st level cache. If that’s the case, it gets it from there.
This is the key element of this workaround. It enables you in the 2nd query to ignore the association between the Book and the Publisher entity. Because Hibernate will get all Book entity objects from the 1st level cache, the association to the Publisher entity will be initialized anyways.
Here you can see the query that gets all Publications of the Author with the firstName Thorben. Thanks to the inheritance mapping and the mapped one-to-many association, this query is very simple.
Query q2 = em.createQuery("SELECT p FROM Publication p JOIN p.author a WHERE a.firstName = :fName", Publication.class);
Let’s try this workaround using the following test case. It first executes the 2 described queries and then writes a log message for each retrieved Publication. If the Publication is a Book, this log message includes the name of the Publisher. And I also included log messages that will show the object reference of the Book entity objects. This will show you that Hibernate always returns the same object instance for the Book entity.
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); } }
As you can see in the log file, Hibernate only executed the 2 expected queries. Even though the 2nd query did not initialize the association between the Book and the Publisher, the lazily fetched association is available. As the logged object references show, Hibernate used the same Book entity object in the result of both queries.
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/
Conclusion
As you can see, Hibernate’s 1st level cache and its guarantee that each Session only uses 1 entity representation for each database record, can be used to create very efficient implementations.
And before you start to worry, this workaround is based on well-documented behaviors and key features of JPA and Hibernate. This is future-proof, and you don’t need to worry about it when updating your Hibernate dependency.
Hi, Thorben. How are you. I have been following your posts and videos for quite a few time. I find them great.
Regarding this post, I am afraid the diagram class has a flaw in the class diagram. It should be Book —> Publisher instead of BlogPost —> Publisher.
Regards
Fixed it. Thanks!