L'héritage JPA @EntityGraph inclut des associations facultatives de sous-classes


12

Étant donné le modèle de domaine suivant, je veux charger tous les Answers, y compris leurs Values et leurs sous-enfants respectifs, et les mettre dans un AnswerDTOpour ensuite les convertir en JSON. J'ai une solution de travail mais elle souffre du problème N + 1 dont je veux me débarrasser en utilisant un ad-hoc @EntityGraph. Toutes les associations sont configurées LAZY.

entrez la description de l'image ici

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

En utilisant un ad-hoc @EntityGraphsur la Repositoryméthode, je peux m'assurer que les valeurs sont pré-récupérées pour empêcher N + 1 sur l' Answer->Valueassociation. Bien que mon résultat soit satisfaisant, il existe un autre problème N + 1, en raison du chargement paresseux de l' selectedassociation de l' MCValueal.

Utiliser ceci

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

échoue, car le selectedchamp n'est bien sûr qu'une partie de certaines des Valueentités:

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

Comment puis-je dire à JPA d'essayer de récupérer l' selectedassociation uniquement si la valeur est a MCValue? J'ai besoin de quelque chose comme ça optionalAttributePaths.

Réponses:


8

Vous pouvez uniquement utiliser un EntityGraphsi l'attribut d'association fait partie de la superclasse et par cela fait également partie de toutes les sous-classes. Sinon, le EntityGraphéchouera toujours avec le Exceptionque vous obtenez actuellement.

La meilleure façon d'éviter votre problème de sélection N + 1 consiste à diviser votre requête en 2 requêtes:

La 1ère requête récupère les MCValueentités à l'aide d'un EntityGraphpour récupérer l'association mappée par l' selectedattribut. Après cette requête, ces entités sont ensuite stockées dans le cache de premier niveau d'Hibernate / le contexte de persistance. Hibernate les utilisera lorsqu'il traitera le résultat de la 2e requête.

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

La 2e requête récupère ensuite l' Answerentité et utilise un EntityGraphpour récupérer également les Valueentités associées . Pour chaque Valueentité, Hibernate instanciera la sous-classe spécifique et vérifiera si le cache de 1er niveau contient déjà un objet pour cette combinaison de classe et de clé primaire. Si tel est le cas, Hibernate utilise l'objet du cache de 1er niveau au lieu des données renvoyées par la requête.

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

Parce que nous avons déjà récupéré toutes les MCValueentités avec les selectedentités associées , nous obtenons maintenant des Answerentités avec une valueassociation initialisée . Et si l'association contient une MCValueentité, son selectedassociation sera également initialisée.


J'ai pensé à avoir deux requêtes, la 1ère pour récupérer les réponses + la valeur et la 2ème pour récupérer selectedles réponses qui ont un MCValue. Je n'aimais pas que cela nécessiterait une boucle supplémentaire et je devrais gérer la correspondance entre les ensembles de données. J'aime votre idée d'exploiter le cache Hibernate pour cela. Pouvez-vous expliquer dans quelle mesure (en termes de cohérence) il est sûr de s'appuyer sur le cache pour contenir les résultats? Est-ce que cela fonctionne lorsque les requêtes sont effectuées dans une transaction? J'ai peur des erreurs d'initialisation paresseuses difficiles à repérer et sporadiques.
Bloqué le

1
Vous devez effectuer les deux requêtes dans la même transaction. Tant que vous faites cela, et que vous n'effacez pas votre contexte de persistance, c'est absolument sûr. Votre cache de 1er niveau contiendra toujours les MCValueentités. Et vous n'avez pas besoin d'une boucle supplémentaire. Vous devez récupérer toutes les MCValueentités avec 1 requête qui se joint à Answeret utilise la même clause WHERE que votre requête actuelle. J'en ai également parlé dans le flux en direct d'aujourd'hui: youtu.be/70B9znTmi00?t=238 Cela a commencé à 3h58 mais j'ai pris quelques autres questions entre les deux ...
Thorben Janssen

Super, merci pour le suivi! Je veux également ajouter que cette solution nécessite 1 requête par sous-classe. La maintenabilité est donc correcte pour nous, mais cette solution pourrait ne pas convenir à tous les cas.
Bloqué le

Je dois corriger un peu mon dernier commentaire: bien sûr, vous n'avez besoin que d'une requête par sous-classe qui souffre du problème. Il convient également de noter que, pour les attributs des sous-classes, cela ne semble pas être un problème, en raison de l'utilisation SINGLE_TABLE_INHERITANCE.
Coincé le

7

Je ne sais pas ce que Spring-Data fait là-bas, mais pour ce faire, vous devez généralement utiliser l' TREATopérateur pour pouvoir accéder à la sous-association, mais l'implémentation de cet opérateur est assez boguée. Hibernate prend en charge l'accès implicite aux propriétés de sous-type, ce dont vous auriez besoin ici, mais apparemment Spring-Data ne peut pas gérer cela correctement. Je peux vous recommander de jeter un œil à Blaze-Persistence Entity-Views , une bibliothèque qui fonctionne au-dessus de JPA qui vous permet de mapper des structures arbitraires sur votre modèle d'entité. Vous pouvez mapper votre modèle DTO d'une manière sûre de type, également la structure d'héritage. Les vues d'entité pour votre cas d'utilisation pourraient ressembler à ceci

@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();
}

Avec l'intégration des données de printemps fournie par Blaze-Persistence, vous pouvez définir un référentiel comme celui-ci et utiliser directement le résultat

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

Il générera une requête HQL qui sélectionne exactement ce que vous avez mappé dans ce AnswerDTOqui ressemble à ce qui suit.

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

Hmm merci pour l'allusion à votre bibliothèque que j'ai déjà trouvée, mais nous ne l'utiliserions pas pour 2 raisons principales: 1) nous ne pouvons pas compter sur la lib à prendre en charge pendant la durée de notre projet (votre entreprise blazebit est plutôt petite et à ses débuts). 2) Nous ne nous engagerions pas dans une pile technologique plus complexe pour optimiser une seule requête. (Je sais que votre bibliothèque peut faire plus, mais nous préférons une pile technologique commune et préférerions simplement implémenter une requête / transformation personnalisée s'il n'y a pas de solution JPA).
Bloqué le

1
Blaze-Persistence est open source et Entity-Views est plus ou moins implémenté par-dessus JPQL / HQL qui est standard. Les fonctionnalités qu'il implémente sont stables et fonctionneront toujours avec les futures versions d'Hibernate, car il fonctionne en plus de la norme. Je comprends que vous ne voulez pas introduire quelque chose à cause d'un seul cas d'utilisation, mais je doute que ce soit le seul cas d'utilisation pour lequel vous pourriez utiliser Entity Views. L'introduction des vues d'entité entraîne généralement une réduction significative de la quantité de code standard et augmente également les performances des requêtes. Si vous ne voulez pas utiliser d'outils qui vous aident, tant pis.
Christian Beikov

Au moins, vous avez compris le problème et vous apportez une solution. Vous obtenez donc la prime même si les réponses n'expliquent pas ce qui se passe dans le problème d'origine et comment JPA pourrait le résoudre. De mon point de vue, il n'est tout simplement pas pris en charge par JPA et cela devrait devenir une demande de fonctionnalité. J'offrirai une autre prime pour une réponse plus élaborée ciblant uniquement JPA.
Bloqué le

Ce n'est tout simplement pas possible avec JPA. Vous avez besoin de l'opérateur TREAT qui n'est entièrement pris en charge par aucun fournisseur JPA, ni pris en charge dans les annotations EntityGraph. Par conséquent, la seule façon de modéliser cela est via la fonction de résolution de propriété de sous-type implicite Hibernate, qui vous oblige à utiliser des jointures explicites.
Christian Beikov

1
Dans votre réponse, la définition de la vue devrait êtreinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Bloqué le

0

Mon dernier projet a utilisé GraphQL (une première pour moi) et nous avons eu un gros problème avec les requêtes N + 1 et essayer d'optimiser les requêtes pour qu'elles ne se joignent aux tables que lorsqu'elles sont requises. J'ai trouvé irremplaçable Cosium / spring-data-jpa-entity-graph . Il étend JpaRepositoryet ajoute des méthodes pour passer un graphe d'entité à la requête. Vous pouvez ensuite créer des graphiques d'entités dynamiques au moment de l'exécution pour ajouter des jointures gauches uniquement aux données dont vous avez besoin.

Notre flux de données ressemble à ceci:

  1. Recevoir la requête GraphQL
  2. Analyser la requête GraphQL et convertir en liste de nœuds de graphe d'entité dans la requête
  3. Créer un graphique d'entité à partir des nœuds découverts et passer dans le référentiel pour exécution

Pour résoudre le problème de l'inclusion de nœuds non valides dans le graphe d'entité (par exemple à __typenamepartir de graphql), j'ai créé une classe utilitaire qui gère la génération du graphe d'entité. La classe appelante transmet le nom de la classe pour laquelle elle génère le graphique, qui valide ensuite chaque nœud du graphique par rapport au métamodèle géré par l'ORM. Si le nœud n'est pas dans le modèle, il le supprime de la liste des nœuds de graphe. (Cette vérification doit être récursive et vérifier également chaque enfant)

Avant de trouver cela, j'avais essayé les projections et toutes les autres alternatives recommandées dans les documents Spring JPA / Hibernate, mais rien ne semblait résoudre le problème avec élégance ou au moins avec une tonne de code supplémentaire.


comment résout-il le problème des associations de chargement qui ne sont pas connues du super type? De plus, comme dit dans l'autre réponse, nous voulons savoir s'il existe une solution JPA pure, mais je pense également que la lib souffre du même problème que l' selectedassociation n'est pas disponible pour tous les sous-types de value.
Coincé le

Si vous êtes intéressé par GraphQL, nous avons également une intégration de Blaze-Persistence Entity Views avec graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov

@ChristianBeikov merci mais nous utilisons SQPR pour générer notre schéma par programme à partir de nos modèles / méthodes
aarbor

Si vous aimez l'approche code-first, vous adorerez l'intégration GraphQL. Il gère la récupération automatique des colonnes / expressions réellement utilisées, ce qui réduit les jointures, etc.
Christian Beikov

0

Modifié après votre commentaire:

Je m'excuse, je n'ai pas sous-entendu votre problème au premier tour, votre problème se produit au démarrage de spring-data, pas seulement lorsque vous essayez d'appeler findAll ().

Ainsi, vous pouvez maintenant parcourir l'exemple complet qui peut être extrait de mon github: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Vous pouvez facilement reproduire et résoudre votre problème dans ce projet.

En effet, les données Spring et hibernate ne sont pas capables de déterminer le graphique "sélectionné" par défaut et vous devez spécifier la manière de collecter l'option sélectionnée.

Donc, tout d'abord, vous devez déclarer les NamedEntityGraphs de la classe Answer

Comme vous pouvez le voir, il existe deux NamedEntityGraph pour la valeur d' attribut de la classe Answer

  • Le premier pour tous Valeur sans relation spécifique avec la charge

  • Le second pour la valeur Multichoice spécifique . Si vous supprimez celui-ci, vous reproduisez l'exception.

Deuxièmement, vous devez être dans un contexte transactionnel answerRepository.findAll () si vous souhaitez récupérer des données de type LAZY

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}

Le problème n'est pas de récupérer l' valueassociation-de Answermais d'obtenir l' selectedassociation au cas où a valueest MCValue. Votre réponse ne contient aucune information à ce sujet.
Bloqué le

@Stuck Merci pour votre réponse, pouvez-vous s'il vous plaît partager avec moi la classe MCValue, je vais essayer de reproduire votre problème localement.
bdzzaid

Votre exemple ne fonctionne que parce que vous avez défini l'association OneToManycomme FetchType.EAGERmais comme indiqué dans la question: toutes les associations le sont LAZY.
Bloqué le

@Stuck J'ai mis à jour ma réponse depuis votre dernière mise à jour, j'espère que ma réponse vous aidera à résoudre votre problème et vous aidera à comprendre la façon de charger un graphique d'entité, y compris des relations facultatives.
bdzzaid

Votre "solution" souffre toujours du problème N + 1 d'origine sur lequel porte cette question: mettez des méthodes d'insertion et de recherche dans différentes transactions de votre test et vous voyez que jpa émettra une requête DB selectedpour chaque réponse au lieu de les charger d'avance.
Bloqué le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.