Ús dels Entity Graphs de JPA com a mecanisme per reduir el nombre d’accessos a una base de dades

1. Introducció

 

En aquest article descriurem l’ús del mecanisme tècnic anomenat Entity Graph (present des de la versió JPA 2.1) amb l’objectiu d’evitar el problema dels accessos N+1, el qual és un dels inconvenients més coneguts i recurrents quant a la utilització de JPA per accedir a una base de dades relacional.

Aquest problema consisteix en què, per obtenir una llista d’ocurrències d’una determinada entitat, a més d’executar la consulta que retorna aquesta llista de resultats (N), si l’entitat en qüestió referència una altra, s’executen N consultes addicionals per obtenir les dades de l’entitat referenciada.

Un Entity Graph consisteix en una especificació del subconjunt del model de dades  -a nivell d’entitats i atributs necessaris-  perquè un determinat servei pugui resoldre la funcionalitat desitjada.

 

 

2. Exemple de model de dades

 

Per il·lustrar l’ús de la tècnica descrita, ens basarem en el següent fragment del model de model de dades utilitzat per l’aplicació PSF (Programa de Suport al Finançament):

 

Com es pot veure, en aquest model de dades apareix un exemple de relació 1-N i un altre de relació N-1.

 

La definició de les entitats implicades és:

  • public class DadesFixes {

               @Id

               @Column(name = "FIX_CODICREDIT")

               private Long fixCodiCredit;

 

               @ManyToOne

               @JoinColumn(name = "FIX_CODIENS", referencedColumnName = "ENS_INE")

               private CuccEns fixEns;

 

               @OneToMany(mappedBy="dadesFixes", fetch=FetchType.LAZY)

               private List<Economiques> economiques;

               ...            

}

 

  • public class CuccEns {

               @Id

               @Column(name = "ENS_ID")

               private Long ensId;

 

               @Column(name = "ENS_INE")

               private String ensIne;

 

               @Column(name = "ENS_NOM")

               private String ensNom;     

               ...            

}

 

  • public class Economiques {

               @EmbeddedId

               private EconomiquesPK economiquesPK;

 

               @ManyToOne(fetch=FetchType.LAZY)

               // Nota: És important declarar com a LAZY la relació inversa d’una relació 1-N.

               // En aquest cas, evita la càrrega de l’entitat DadesFixes per a cada

               // registre Economiques obtingut.

               @JoinColumn(name = "ECO_CODICREDIT", insertable = false, updatable = false)

               private DadesFixes dadesFixes;

               ...            

}

 

public class EconomiquesPK

{

               @Column(name = "ECO_CODICREDIT")

               private Long ecoCodiCredit;

 

               @Column(name = "ECO_NUMACT", insertable = false, updatable = false)

               private Long ecoNumAct;

 

               @Temporal(TemporalType.DATE)

               @Column(name = "ECO_DATATRAMIT")

               private Date ecoDataTramit;

}

 

 

 

3. Identificació del problema

 

Per tal de detectar l’execució dels ‘accessos N+1’, por ser convenient activar la traça de les consultes SQL generades. Amb aquest objectiu, pot ser útil descomentar el següent fragment del fitxer persistence.xml:

      <property name="eclipselink.logging.level.sql" value="FINE"/>

      <property name="eclipselink.logging.timestamp" value="true"/>

      <property name="eclipselink.logging.parameters" value="true"/>

      <property name="eclipselink.logging.logger" value="DefaultLogger"/>     

 

 

En el cas de l’aplicació PSF, es pot veure que es generen les següents consultes en el moment d’obtenir el llistat de crèdits:

 

// 1 query per obtenir la llista de crèdits:

SELECT FIX_CODICREDIT AS a1, FIX_CODIENS AS a37, .. FROM PSF_DADESFIXES

 

// N queries per obtenir els municipis de cadascun dels crèdits recuperats:

SELECT ENS_ID, ENS_INE, ENS_NOM FROM CUCC_ENS WHERE (ENS_INE = codi_crèdit_1)

..

SELECT ENS_ID, ENS_INE, ENS_NOM FROM CUCC_ENS WHERE (ENS_INE = codi_crèdit_N)

 

// N queries per obtenir les dades econòmiques de cadascun dels crèdits recuperats:

SELECT ECO_CODICREDIT, ECO_NUMACT, ... FROM PSF_ECONOMIQUES WHERE (ECO_CODICREDIT = codi_crèdit_1)

...

SELECT ECO_CODICREDIT, ECO_NUMACT, ... FROM PSF_ECONOMIQUES WHERE (ECO_CODICREDIT = codi_crèdit_N)

 

Es pot apreciar que en aquest cas s’estan executant 2N+1 consultes, una per obtenir la llista de crèdits, N per obtenir cadascun dels municipis referenciats, i N per obtenir les dades econòmiques de cadascun dels crèdits obtinguts.

 

 

 

4. Procediment de definició de l’Entity Graph per reduir el nombre de consultes generades

 

Per explicar com definir un Entity Graph, utilitzarem com a exemple la consulta que s’executa al polsar el botó 'Cerca' de la pantalla llista de crèdits de l’aplicació PSF:   (dadesFixesLlista.xhtml)

 

En aquesta consulta intervenen els següents atributs, els quals s’han identificat a partir de les columnes i filtres referenciats des d’aquesta pantalla:

 

Filtre

Atribut referenciat pel filtre

Entitat

dadesFixes.fixEns.ensId

Codi crèdit

dadesFixes.fixCodiCredit

Descripció

dadesFixes.fixDesc

 

Columna

Atribut referenciat per la columna

Codi crèdit

dadesFixes.fixCodiCredit

Descripció del crèdit

dadesFixes.fixDesc

Entitat

dadesFixes.fixEns.ensNom

Total disposat

dadesFixes.fixImportSol

Data disposició del crèdit

dadesFixes.economiques[0].economiquesPK.ecoDataTramit

 

A partir de l’anàlisi dels atributs que intervenen en la consulta, es desprén que la definió de l’Entity Graph corresponent a la part del model de dades a accedir és la següent:

 

@NamedEntityGraph(

    name="graph.dadesFixes",

    attributeNodes={

        @NamedAttributeNode("fixCodiCredit"),

        @NamedAttributeNode("fixDesc"),

        @NamedAttributeNode("fixImportSol"),

        @NamedAttributeNode(value="fixEns", subgraph="subgraph.fixEns"),

        @NamedAttributeNode(value="economiques")                  

    },

    subgraphs={

        @NamedSubgraph(

            name="subgraph.fixEns",

            attributeNodes={

                    @NamedAttributeNode("ensIne"),

                    @NamedAttributeNode("ensNom")

            }

        )

    }

)

// Nota: Com que no s’ha definit cap subgraph per l’atribut ‘economiques’, es recuperaran

// tots els atributs de l’entitat Economiques.

// Nota: Els camps que pertanyen a la clau primària, per exemple ‘ensId’, no calen ser especificats.

public class DadesFixes {

      ...

 

Nota: Un entity graph es defineix sobre l’entitat principal a tractar per la consulta que es pretén optimitzar, en aquest cas ‘DadesFixes’.

 

 

 

5. Ús des de la capa de servei de l’entity graph definit

 

Per referenciar a un determinat entity graph des d’un servei, es pot sobre escriure el mètode ‘setHints’ del servei en qüestió tal i com es mostra a continuació:

Nota: Aquest mètode es cridat des de ‘DibaService.findAll()’ just abans de fer el “fetch” que recupera les dades.

 

/**

 * Estableix l'estratègia d'accèss a usar.

*/   

@Override

public void setHints( JPAQuery<DadesFixes> query )

{          

query.setHint("javax.persistence.fetchgraph", entityManagerHelper.getEntityGraph("graph.dadesFixes"));

query.setHint("eclipselink.left-join-fetch", "dadesFixes.fixEns");

query.setHint("eclipselink.left-join-fetch", "dadesFixes.economiques");

}

 

Nota: L’objectiu dels dos últims “hints” és obtenir la llista de resultats amb una única consulta.

 

Veure la resta de hints proporcionats per EclipseLink a:

https://www.eclipse.org/eclipselink/documentation/2.6/jpa/extensions/queryhints.htm

 

 

 

 

6. Consultes SQL generades

 

Amb la utilització de l’entity graph definit, la consulta resultant és la següent:

 

SELECT t1.FIX_CODICREDIT AS a1,

  t1.FIX_DESC            AS a2,

  t1.FIX_IMPORTSOL       AS a3,

  t1.FIX_CODIENS         AS a4,

  t0.ENS_ID              AS a5,

  t0.ENS_INE             AS a6,

  t0.ENS_NOM             AS a7,

  t2.ECO_DATATRAMIT      AS a8,

  t2.ECO_CODICREDIT      AS a9,

  t2.ECO_NUMACT          AS a10,

  t2...

FROM PSF_DADESFIXES t1

LEFT OUTER JOIN CUCC_ENS t0

     ON (t0.ENS_INE = t1.FIX_CODIENS)

LEFT OUTER JOIN PSF_ECONOMIQUES t2

     ON (t2.ECO_CODICREDIT = t1.FIX_CODICREDIT)

ORDER BY t1.FIX_CODICREDIT DESC

 

 

Notes:

  • L’ús d’un Entity Graph associat a una consulta amb paginació que inclou una relació 1-N no funciona del tot bé en la versió d’EclipseLink utilitzada (2.6.5), ja que en determinat casos, el servei termina retornant excepció, o bé, termina correctament però amb un nombre de resultats menor de l’esperat. Suposo que això es degut a que existeixen “bugs” en la implementació d’EclipseLink.
  • Com a solució a aquest problema, es pot seguir una de les següents estratègies:

1. Evitar la paginació en la consulta a executar: Aquesta tècnica es factible si s’estima que el nombre de registres a retornar no es massa elevat.

Per implementar aquesta estratègia, comentar la següent línia del servei en qüestió:

if (rowCount > 0) {

// Evita la limitació del nombre de registres a retornar:

// query = query.limit(rowCount);

}

     

2. Prescindir de fer el JOIN amb l’entitat referenciada:

Es pot implementar comentant la següent línia del servei en qüestió:

//query.setHint("eclipselink.left-join-fetch", "dadesFixes.economiques");

En aquest cas, s’executen N consultes sobre l’entitat referenciada (Economiques), encara que el temps total d’execució pot ser menor que si no s’especifiqués la relació amb ‘Economiques’ des de l’entity graph definit.

 

 

 

7. Comparativa entre temps d’execució

 

La següent gràfica compara el temps (en mil·lisegons) emprat per la consulta que obté el llistat de crèdits quan s’usa l’Entity graph definit, respecte del temps emprat quan no s’utilitza:

 

 

S’observa que, en el cas de no usar l’Entity Graph, el comportament de la gràfica és lineal mentre que en el cas d’usar-lo és gairebé costant, la qual cosa implica que la utilització dels Entity Graphs resulta més convenient en aquelles situacions en què el nombre de registres a recuperar és relativament gran.

 

Extracte dels valors obtinguts:

Nombre de resultats

Temps sense Entity Graph (en mil·lisegons)

Temps amb Entity Graph (en mil·lisegons)

20

172

45

40

365

64

60

580

74

80

743

64

100

1040

59

 

 

8. Conclusió

 

La tècnica descrita pot permetre reduir significativament el nombre de consultes necessàries per accedir a un model de dades quan s’usa JPA, la qual cosa pot impactar positivament, no tant sols quant a la reducció del temps de resposta d’una determinada funcionalitat, sinó també pel que fa a evitar la sobre càrrega de recursos compartits entre diverses funcionalitats o aplicacions, com ara, el servidor de base de dades, el servidor d’aplicacions o la xarxa de comunicacions utilitzada.

 

1
Categories:
Plataforma JEE
1
Grups de treball:
Plataforma JEE