Grafico entità JPA

1. Panoramica

JPA 2.1 ha introdotto la funzione Entity Graph come metodo più sofisticato per gestire il caricamento delle prestazioni.

Permette di definire un template raggruppando i relativi campi di persistenza che vogliamo recuperare e ci permette di scegliere il tipo di grafico a runtime.

In questo tutorial, spiegheremo più in dettaglio come creare e utilizzare questa funzionalità.

2. Cosa cerca di risolvere il grafico delle entità

Fino a JPA 2.0, per caricare un'associazione di entità, di solito usavamo FetchType. LAZY e FetchType. EAGER come strategie di recupero . Questo indica al provider JPA di recuperare o meno l'associazione correlata. Sfortunatamente, questa meta configurazione è statica e non consente di passare da una strategia all'altra in fase di esecuzione.

L'obiettivo principale di JPA Entity Graph è quindi migliorare le prestazioni di runtime durante il caricamento delle associazioni correlate dell'entità e dei campi di base.

In breve, il provider JPA carica tutto il grafico in una query di selezione e quindi evita il recupero dell'associazione con più query SELECT. Questo è considerato un buon approccio per migliorare le prestazioni dell'applicazione.

3. Definizione del modello

Prima di iniziare a esplorare Entity Graph, dobbiamo definire le entità del modello con cui stiamo lavorando. Supponiamo di voler creare un sito blog in cui gli utenti possono commentare e condividere post.

Quindi, prima avremo un'entità utente :

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

L'utente può condividere vari post, quindi abbiamo anche bisogno di un'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'utente può anche commentare i post condivisi, quindi, infine, aggiungeremo un'entità Commento :

@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; //... }

Come possiamo vedere, l' entità Post ha un'associazione con le entità Commento e Utente . L' entità Commento ha un'associazione alle entità Post e Utente .

L'obiettivo è quindi caricare il grafico seguente utilizzando vari modi:

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

4. Caricamento di entità correlate con strategie FetchType

Il metodo FetchType definisce due strategie per il recupero dei dati dal database:

  • FetchType.EAGER : il provider di persistenza deve caricare il campo o la proprietà annotati correlati. Questo è il comportamento predefinito per icampi annotati @Basic, @ManyToOne e @OneToOne .
  • FetchType.LAZY : il provider di persistenza dovrebbe caricare i dati al primo accesso, ma può essere caricato con impazienza. Questo è il comportamento predefinito per icampi annotati @OneToMany, @ManyToMany e @ ElementCollection .

Ad esempio, quando carichiamo un'entità Post , le entità Comment correlate non vengono caricate come FetchType predefinito poiché @OneToMany è LAZY. Possiamo sovrascrivere questo comportamento modificando FetchType in EAGER:

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

In confronto, quando carichiamo un'entità Comment , la sua entità padre Post viene caricata come modalità predefinita per @ManyToOne, che è EAGER. Possiamo anche scegliere di non caricare l' entità Post modificando questa annotazione in LAZY:

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

Si noti che poiché LAZY non è un requisito, il provider di persistenza può comunque caricare l' entità Post con entusiasmo se lo desidera. Quindi, per utilizzare correttamente questa strategia, dovremmo tornare alla documentazione ufficiale del provider di persistenza corrispondente.

Ora, perché abbiamo usato le annotazioni per descrivere la nostra strategia di recupero, la nostra definizione è statico e non c'è modo per passare da una LAZY e EAGER in fase di esecuzione .

È qui che entra in gioco l'Entity Graph, come vedremo nella prossima sezione.

5. Definizione di un grafico di entità

Per definire un Entity Graph, possiamo utilizzare le annotazioni sull'entità oppure procedere in modo programmatico utilizzando l'API JPA.

5.1. Definizione di un grafico di entità con annotazioni

L' annotazione @ NamedEntityGraph consente di specificare gli attributi da includere quando vogliamo caricare l'entità e le relative associazioni.

Quindi definiamo prima un Entity Graph che carica il Post e le sue entità correlate Utente e Commenti :

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

In questo esempio, abbiamo utilizzato @NamedAttributeNode per definire le entità correlate da caricare quando viene caricata l'entità radice.

Definiamo ora un Entity Graph più complicato dove vogliamo caricare anche gli User relativi ai Commenti .

A questo scopo, utilizzeremo l' attributo del sottografo @NamedAttributeNode . Ciò consente di fare riferimento a un sottografo denominato definito tramite l' annotazione @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 definizione dell'annotazione @NamedSubgraph è simile a @ NamedEntityGraph e consente di specificare gli attributi dell'associazione correlata. In questo modo, possiamo costruire un grafico completo.

Nell'esempio sopra, con il grafico definito ' post-entity-graph-with-comment-users' , possiamo caricare il Post, il relativo Utente, i Commenti e gli Utenti relativi ai Commenti.

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.

Le prestazioni sono ovviamente un fattore chiave da tenere in considerazione quando si progettano le entità JPA. La documentazione JPA consiglia di utilizzare la strategia FetchType.LAZY quando possibile e l'Entity Graph quando è necessario caricare un'associazione.

Come al solito, tutto il codice è disponibile su GitHub.