Search code examples
javapostgresqlhibernatejpaentitygraph

JPA inheritance @EntityGraph include optional associations of subclasses


Given the following domain model, I want to load all Answers including their Values and their respective sub-children and put it in an AnswerDTO to then convert to JSON. I have a working solution but it suffers from the N+1 problem that I want to get rid of by using an ad-hoc @EntityGraph. All associations are configured LAZY.

enter image description here

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Using an ad-hoc @EntityGraph on the Repository method I can ensure that the values are pre-fetched to prevent N+1 on the Answer->Value association. While my result is fine there is another N+1 problem, because of lazy loading the selected association of the MCValues.

Using this

@EntityGraph(attributePaths = {"value.selected"})

fails, because the selected field is of course only part of some of the Value entities:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

How can I tell JPA only try fetching the selected association in case the value is a MCValue? I need something like optionalAttributePaths.


Solution

  • You can only use an EntityGraph if the association attribute is part of the superclass and by that also part of all subclasses. Otherwise, the EntityGraph will always fail with the Exception that you currently get.

    The best way to avoid your N+1 select issue is to split your query into 2 queries:

    The 1st query fetches the MCValue entities using an EntityGraph to fetch the association mapped by the selected attribute. After that query, these entities are then stored in Hibernate's 1st level cache / the persistence context. Hibernate will use them when it processes the result of the 2nd query.

    @Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
    @EntityGraph(attributePaths = {"selected"})
    public List<MCValue> findAll();
    

    The 2nd query then fetches the Answer entity and uses an EntityGraph to also fetch the associated Value entities. For each Value entity, Hibernate will instantiate the specific subclass and check if the 1st level cache already contains an object for that class and primary key combination. If that's the case, Hibernate uses the object from the 1st level cache instead of the data returned by the query.

    @Query("SELECT a FROM Answer a")
    @EntityGraph(attributePaths = {"value"})
    public List<Answer> findAll();
    

    Because we already fetched all MCValue entities with the associated selected entities, we now get Answer entities with an initialized value association. And if the association contains an MCValue entity, its selected association will also be initialized.