Graphique d'entité JPA

1. Vue d'ensemble

JPA 2.1 a introduit la fonction Entity Graph comme une méthode plus sophistiquée pour gérer le chargement des performances.

Il permet de définir un modèle en regroupant les champs de persistance associés que nous voulons récupérer et nous laisse choisir le type de graphe à l'exécution.

Dans ce didacticiel, nous expliquerons plus en détail comment créer et utiliser cette fonctionnalité.

2. Ce que le graphe d'entité tente de résoudre

Jusqu'à JPA 2.0, pour charger une association d'entité, nous utilisions généralement FetchType. LAZY et FetchType. EAGER comme stratégies de récupération . Cela demande au fournisseur JPA d'extraire ou non l'association associée. Malheureusement, cette méta configuration est statique et ne permet pas de basculer entre ces deux stratégies à l'exécution.

L'objectif principal du graphe d'entité JPA est alors d'améliorer les performances d'exécution lors du chargement des associations associées et des champs de base de l'entité.

En bref, le fournisseur JPA charge tout le graphique dans une requête de sélection, puis évite de récupérer l'association avec plus de requêtes SELECT. Ceci est considéré comme une bonne approche pour améliorer les performances des applications.

3. Définition du modèle

Avant de commencer à explorer le graphique d'entité, nous devons définir les entités de modèle avec lesquelles nous travaillons. Disons que nous voulons créer un site de blog sur lequel les utilisateurs peuvent commenter et partager des articles.

Donc, nous aurons d'abord une entité utilisateur :

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; //... }

L'utilisateur peut partager divers articles, nous avons donc également besoin d'une entité Post :

@Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String subject; @OneToMany(mappedBy = "post") private List comments = new ArrayList(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private User user; //... }

L'utilisateur peut également commenter les publications partagées, donc, enfin, nous ajouterons une entité Comment :

@Entity public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String reply; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private Post post; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private User user; //... }

Comme nous pouvons le voir, l' entité Post a une association avec les entités Comment et User . L' entité Comment a une association avec les entités Post et User .

Le but est alors de charger le graphique suivant de différentes manières:

Post -> user:User -> comments:List comments[0]:Comment -> user:User comments[1]:Comment -> user:User

4. Chargement d'entités associées avec des stratégies FetchType

La méthode FetchType définit deux stratégies pour récupérer les données de la base de données:

  • FetchType.EAGER : le fournisseur de persistance doit charger le champ ou la propriété annoté associé. Il s'agit du comportement par défaut pour leschamps annotés @Basic, @ManyToOne et @OneToOne .
  • FetchType.LAZY : Le fournisseur de persistance doit charger les données lors de son premier accès, mais peut être chargé avec empressement. C'est le comportement par défaut pour leschamps annotés @OneToMany, @ManyToMany et @ ElementCollection .

Par exemple, lorsque nous chargeons une entité Post , les entités Comment associées ne sont pas chargées en tant que FetchType par défaut puisque @OneToMany est LAZY. Nous pouvons remplacer ce comportement en modifiant le FetchType en EAGER:

@OneToMany(mappedBy = "post", fetch = FetchType.EAGER) private List comments = new ArrayList();

Par comparaison, lorsque nous chargeons une entité Comment , son entité parent Post est chargée comme mode par défaut pour @ManyToOne, qui est EAGER. Nous pouvons également choisir de ne pas charger l' entité Post en modifiant cette annotation en LAZY:

@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post;

Notez que comme LAZY n'est pas une exigence, le fournisseur de persistance peut toujours charger l' entité Post avec empressement s'il le souhaite. Donc, pour utiliser correctement cette stratégie, nous devons revenir à la documentation officielle du fournisseur de persistance correspondant.

Maintenant, parce que nous avons utilisé des annotations pour décrire notre stratégie de récupération, notre définition est statique et il n'y a aucun moyen de basculer entre LAZY et EAGER au moment de l'exécution .

C'est là que le graphique d'entité entre en jeu comme nous le verrons dans la section suivante.

5. Définition d'un graphique d'entité

Pour définir un graphique d'entité, nous pouvons soit utiliser les annotations sur l'entité, soit procéder par programmation à l'aide de l'API JPA.

5.1. Définition d'un graphique d'entité avec des annotations

L' annotation @ NamedEntityGraph permet de spécifier les attributs à inclure lorsque nous voulons charger l'entité et les associations associées.

Définissons donc d'abord un graphique d'entité qui charge la publication et ses entités associées User et Comment s:

@NamedEntityGraph( name = "post-entity-graph", attributeNodes = { @NamedAttributeNode("subject"), @NamedAttributeNode("user"), @NamedAttributeNode("comments"), } ) @Entity public class Post { @OneToMany(mappedBy = "post") private List comments = new ArrayList(); //... }

Dans cet exemple, nous avons utilisé le @NamedAttributeNode pour définir les entités associées à charger lorsque l'entité racine est chargée.

Définissons maintenant un graphique d'entité plus compliqué où nous voulons également charger les utilisateurs liés aux commentaires .

Pour cela, nous utiliserons l' attribut de sous-graphe @NamedAttributeNode . Cela permet de référencer un sous-graphe nommé défini via l' annotation @NamedSubgraph :

@NamedEntityGraph( name = "post-entity-graph-with-comment-users", attributeNodes = { @NamedAttributeNode("subject"), @NamedAttributeNode("user"), @NamedAttributeNode(value = "comments", subgraph = "comments-subgraph"), }, subgraphs = { @NamedSubgraph( name = "comments-subgraph", attributeNodes = { @NamedAttributeNode("user") } ) } ) @Entity public class Post { @OneToMany(mappedBy = "post") private List comments = new ArrayList(); //... }

La définition de l' annotation @NamedSubgraph est similaire à celle de @ NamedEntityGraph et permet de spécifier les attributs de l'association associée. Ce faisant, nous pouvons construire un graphique complet.

Dans l'exemple ci-dessus, avec le graphique défini ' post-entity-graph-with-comment-users' , nous pouvons charger le message, l' utilisateur associé , les commentaires et les utilisateurs liés aux commentaires.

Finally, note that we can alternatively add the definition of the Entity Graph using the orm.xml deployment descriptor:

  ...     ... 

5.2. Defining an Entity Graph with the JPA API

We can also define the Entity Graph through the EntityManager API by calling the createEntityGraph() method:

EntityGraph entityGraph = entityManager.createEntityGraph(Post.class);

To specify the attributes of the root entity, we use the addAttributeNodes() method.

entityGraph.addAttributeNodes("subject"); entityGraph.addAttributeNodes("user");

Similarly, to include the attributes from the related entity, we use the addSubgraph() to construct an embedded Entity Graph and then we the addAttributeNodes() as we did above.

entityGraph.addSubgraph("comments") .addAttributeNodes("user");

Now that we have seen how to create the Entity Graph, we'll explore how to use it in the next section.

6. Using the Entity Graph

6.1. Types of Entity Graphs

JPA defines two properties or hints by which the persistence provider can choose in order to load or fetch the Entity Graph at runtime:

  • javax.persistence.fetchgraph – Only the specified attributes are retrieved from the database. As we are using Hibernate in this tutorial, we can note that in contrast to the JPA specs, attributes statically configured as EAGER are also loaded.
  • javax.persistence.loadgraph – In addition to the specified attributes, attributes statically configured as EAGER are also retrieved.

In either case, the primary key and the version if any are always loaded.

6.2. Loading an Entity Graph

We can retrieve the Entity Graph using various ways.

Let's start by using the EntityManager.find() method. As we've already shown, the default mode is based on the static meta-strategies FetchType.EAGER and FetchType.LAZY.

So let's invoke the find() method and inspect the log:

Post post = entityManager.find(Post.class, 1L);

Here is the log provided by Hibernate implementation:

select post0_.id as id1_1_0_, post0_.subject as subject2_1_0_, post0_.user_id as user_id3_1_0_ from Post post0_ where post0_.id=?

As we can see from the log, the User and Comment entities are not loaded.

We can override this default behavior by invoking the overloaded find() method which accepts hints as a Map. We can then provide the graph type which we want to load:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph"); Map properties = new HashMap(); properties.put("javax.persistence.fetchgraph", entityGraph); Post post = entityManager.find(Post.class, id, properties);

If we look again in the log, we can see that these entities are now loaded and only in one select query:

select post0_.id as id1_1_0_, post0_.subject as subject2_1_0_, post0_.user_id as user_id3_1_0_, comments1_.post_id as post_id3_0_1_, comments1_.id as id1_0_1_, comments1_.id as id1_0_2_, comments1_.post_id as post_id3_0_2_, comments1_.reply as reply2_0_2_, comments1_.user_id as user_id4_0_2_, user2_.id as id1_2_3_, user2_.email as email2_2_3_, user2_.name as name3_2_3_ from Post post0_ left outer join Comment comments1_ on post0_.id=comments1_.post_id left outer join User user2_ on post0_.user_id=user2_.id where post0_.id=?

Let's see how we can achieve the same thing using JPQL:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users"); Post post = entityManager.createQuery("select p from Post p where p.id = :id", Post.class) .setParameter("id", id) .setHint("javax.persistence.fetchgraph", entityGraph) .getSingleResult();

And finally, let's have a look at a Criteria API example:

EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users"); CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Post.class); Root root = criteriaQuery.from(Post.class); criteriaQuery.where(criteriaBuilder.equal(root.get("id"), id)); TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); typedQuery.setHint("javax.persistence.loadgraph", entityGraph); Post post = typedQuery.getSingleResult();

In each of these, the graph type is given as a hint. While in the first example we used the Map, in the two later examples we've used the setHint() method.

7. Conclusion

In this article, we've explored using the JPA Entity Graph to dynamically fetch an Entity and its associations.

The decision is made at runtime in which we choose to load or not the related association.

La performance est évidemment un facteur clé à prendre en compte lors de la conception des entités JPA. La documentation JPA recommande d'utiliser la stratégie FetchType.LAZY chaque fois que cela est possible, et le graphique d'entité lorsque nous devons charger une association.

Comme d'habitude, tout le code est disponible sur sur GitHub.