Given the following domain model, I want to load all Answer
s including their Value
s 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
.
@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 MCValue
s.
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
.
In addition to the shared attributes, the book also stores the number of pages, and the blog post persists its URL. JPA and Hibernate support 4 inheritance strategies which map the domain objects to different table structures. The mapped superclass strategy is the simplest approach to mapping an inheritance structure to database tables.
That often becomes an issue, if you try to map these models to a relational database. SQL doesn’t support this kind of relationship and Hibernate, or any other JPA implementation has to map it to a supported concept. You can choose between 4 strategies that map the inheritance structure of your domain model to different table structures.
The @DiscriminatorValue annotation is optional if you use Hibernate. If you don’t provide a discriminator value, Hibernate will use the simple entity name by default. But this default handling isn’t defined by the JPA specification, and you shouldn’t rely on it. … …
When you persist all entities in the same table, Hibernate needs a way to determine the entity class each record represents. This is information is stored in a discriminator column which is not an entity attribute.
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.
I don't know what Spring-Data is doing there, but to do that, you usually have to use the TREAT
operator to be able to access the sub-association but the implementation for that Operator is quite buggy.
Hibernate supports implicit subtype property access which is what you would need here, but apparently Spring-Data can't handle this properly. I can recommend that you take a look at Blaze-Persistence Entity-Views, a library that works on top of JPA which allows you map arbitrary structures against your entity model. You can map your DTO model in a type safe way, also the inheritance structure. Entity views for your use case could look like this
@EntityView(Answer.class)
interface AnswerDTO {
@IdMapping
Long getId();
ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
@IdMapping
Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
@Mapping("selected.id")
Set<Long> getOption();
}
With the spring data integration provided by Blaze-Persistence you can define a repository like this and directly use the result
@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
List<AnswerDTO> findAll();
}
It will generate a HQL query that selects just what you mapped in the AnswerDTO
which is something like the following.
SELECT
a.id,
v.id,
TYPE(v),
CASE WHEN TYPE(v) = TextValue THEN v.text END,
CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
My latest project used GraphQL (a first for me) and we had a big issue with N+1 queries and trying to optimize the queries to only join for tables when they are required. I have found Cosium
/
spring-data-jpa-entity-graph irreplaceable. It extends JpaRepository
and adds methods to pass in an entity graph to the query. You can then build dynamic entity graphs at runtime to add in left joins for only the data you need.
Our data flow looks something like this:
To solve the problem of not including invalid nodes into the entity graph (for example __typename
from graphql), I created a utility class which handles the entity graph generation. The calling class passes in the class name it is generating the graph for, which then validates each node in the graph against the metamodel maintained by the ORM. If the node is not in the model, it removes it from the list of graph nodes. (This check needs to be recursive and check each child as well)
Before finding this I had tried projections and every other alternative recommended in the Spring JPA / Hibernate docs, but nothing seemed to solve the problem elegantly or at least with a ton of extra code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With