Optimizando relaciones entre entidades en Hibernate
En esta entrada, siguiendo el proyecto de ejemplo que teneis en https://github.com/chuchip/jpajoins explicando como optimizar las consultas a la base de datos usando JPA.
Se verán diferentes tipos de consultas, explicando como realizar uniones entre tablas de modo perezoso (lazy) o agresivo (eager). Se unirán tablas por un solo campo, por varios e incluso por uno pero añadiendo una condición estática.
EL proyecto de ejemplo esta desarrollado en Spring Boot con Hibernate, usando como base de datos H2.
Las tablas están definidas en el fichero schema.sql
y se cargan datos para pruebas en el fichero data.sql
Este es el esquema de la base de datos:
En esa ocasión no voy a explicar el código pues es muy parecido al de otros programas que he explicado detalladamente en otras entradas. Me centrare en como realizar las diferentes consultas realizadas en el programa.
Nota aclaratoria:
Si el nombre de la entidad o de la columna en las clases Java tiene una mayúscula en medio, JPA interpretara que su hay un guion en medio y ese será la tabla o columna que buscara en la base de datos. De esta manera si a la clase
Invoiceheader.java
le renombráramos aInvoiceHeader.java
Hibernate, buscaría la tabla invoice_header en la base de datos y fallaría pues no la encontraría.Como ejemplo se puede ver el campo line_details de la tabla invoiceDetails, que en la clase Invoicedetails.java (obsérvese que la D es minúscula) es llamada con la variable lineDetails.
Realizando una ‘select join‘ entre la tabla cabeceras de factura (invoiceHeader) y clientes (customer)
- Enlace perezoso
Al realizar la búsqueda la consulta resultante sera:
select
invoicehea0_.id as id1_3_0_,
invoicehea0_.customerid as customer2_3_0_,
invoicehea0_.fiscalyear as fiscalye3_3_0_,
invoicehea0_.numberinvoice as numberin4_3_0_
from
invoiceheader invoicehea0_
where
invoicehea0_.id=?
y cuando se realice un consulta sobre la columna customer ejecutara la sentencia select necesaria para buscar los datos del cliente:
select
customer0_.id as id1_1_0_,
customer0_.active as active2_1_0_,
customer0_.address as address3_1_0_,
customer0_.name as name4_1_0_
from
customer customer0_
where
customer0_.id=?
Enlace duro
@ManyToOne(fetch=FetchType.EDGER) @JoinColumn(name=“customerid”,referencedColumnName=“id”) Customer customer;
Al estar el tipo de búsqueda establecido a FetchType.EDGER
realizara una única select con su correspondiente left outer join
select
invoicehea0_.id as id1_3_0_,
invoicehea0_.customerid as customer2_3_0_,
invoicehea0_.fiscalyear as fiscalye3_3_0_,
invoicehea0_.numberinvoice as numberin4_3_0_,
customer1_.id as id1_1_1_,
customer1_.active as active2_1_1_,
customer1_.address as address3_1_1_,
customer1_.name as name4_1_1_
from
invoiceheader invoicehea0_
left outer join
customer customer1_
on invoicehea0_.customerid=customer1_.id
where
invoicehea0_.id=?
- Rizando el rizo.Añadiendo valores fijos
Pero, ¿y si queremos que nos enlace las dos tablas por una columna y además con un valor fijo en otra?
En la tabla customer
se definió la columna active y queremos que solo nos muestre los datos de la factura cuando el valor de esa columna sea 1
Para ello necesitaremos la ayuda de la etiqueta @JoinColumnsOrFormulas que nos permite realizar uniones tanto entre dos columnas como estableciendo valores a la columna de la tabla destino (en este caso customer)
@ManyToOne(fetch=FetchType.EDGER)
@JoinColumnsOrFormulas({
@JoinColumnOrFormula(column=@JoinColumn(name="customerid", referencedColumnName ="id") ),
@JoinColumnOrFormula(formula = @JoinFormula(value="1",referencedColumnName = "active"))
})
Customer customer;
La select ejecutada será:
select
invoicehea0_.id as id1_3_0_,
invoicehea0_.customerid as customer2_3_0_,
invoicehea0_.fiscalyear as fiscalye3_3_0_,
invoicehea0_.numberinvoice as numberin4_3_0_,
1 as formula1_0_,
customer1_.id as id1_1_1_,
customer1_.active as active2_1_1_,
customer1_.address as address3_1_1_,
customer1_.name as name4_1_1_
from
invoiceheader invoicehea0_
left outer join
customer customer1_
on invoicehea0_.customerid=customer1_.id
and 1=customer1_.active
where
invoicehea0_.id=?
En el caso de que no encuentre ningún registro, la variable customer tendrá un valor nulo.
Si el tipo de enlace fuera lazy como en el caso anterior se haría primero una query sobre la tabla invoiceheader y cuando se pidiera el valor de la variable customer se realizaría sobre su correspondiente tabla.
Uniendo tabla cabeceras facturas y líneas de facturas.
Para unir las dos tablas pondremos el siguiente código en la clase Invoicedetails.java
Como se ve, al ser dos campos los que unen ambas tablas haremos uso de la etiqueta @JoinColumns con sus correspondientes @JoinColumn dentro.
Como no hemos especificado nada, la unión se hará del tipo EAGER por lo cual la consulta realizada a la base de datos será la siguiente:
select
invoicehea0_.id as id1_3_0_,
invoicehea0_.customerid as customer2_3_0_,
invoicehea0_.fiscalyear as fiscalye3_3_0_,
invoicehea0_.numberinvoice as numberin4_3_0_,
1 as formula1_0_,
details1_.fiscalyear as fiscalye2_2_1_,
details1_.numberinvoice as numberin5_2_1_,
details1_.id as id1_2_1_,
details1_.id as id1_2_2_,
details1_.articleid as articlei6_2_2_,
details1_.fiscalyear as fiscalye2_2_2_,
details1_.linea_details as linea_de3_2_2_,
details1_.numberarticles as numberar4_2_2_,
details1_.numberinvoice as numberin5_2_2_,
article2_.id as id1_0_3_,
article2_.description as descript2_0_3_,
article2_.price as price3_0_3_
from
invoiceheader invoicehea0_
left outer join
invoicedetails details1_
on invoicehea0_.fiscalyear=details1_.fiscalyear
and invoicehea0_.numberinvoice=details1_.numberinvoice
left outer join
article article2_
on details1_.articleid=article2_.id
where
invoicehea0_.id=?
El ultimo “left outer join” haciendo referencia a la tabla article lo pone Hibernate porque en la clase Invoicedetails.java tenemos el código:
@ManyToOne(fetch=FetchType.EAGER)
@JoinColumns({
@JoinColumn(name="articleid",referencedColumnName="id")
})
Article articles;
para que nos muestre los datos del articulo por cada línea del articulo, y como esta marcada la unión a tipo EAGER, Hibernate es lo suficientemente listo para hacer una sola consulta a la base de datos.
Si realizamos un llamada a http://localhost:8080/1 observaremos la siguiente salida que devuelve la clase Invoiceheader.java veremos lo siguiente:
{
"id": 1,
"yearFiscal": 2019,
"numberInvoice": 1,
"customerId": 1,
"customer": {
"id": 1,
"name": "customer 1 name",
"address": "customer 1 address",
"active": 1
},
"details": [
{
"id": 2,
"year": 2019,
"invoice": 1,
"linea": 2,
"numberarticles": 3,
"articles": {
"id": 2,
"description": "article 2 description",
"price": 12.3
}
},
{
"id": 1,
"year": 2019,
"invoice": 1,
"linea": 1,
"numberarticles": 5,
"articles": {
"id": 1,
"description": "article 1 description",
"price": 10.1
}
}
]
}
¿ No veis algo raro ?. ¡Efectivamente!. Las lineas las saca ordenadas de mayor a menor (primero la linea 2, luego la 1). Evidentemente eso es debido a la etiqueta @OrderBy(“linea desc”) , de la cual hay que destacar dos cosas:
- El campo al que hacemos referencia es como es nombrado en la entidad. Así aunque en la base de datos se llama lineaDetails se referencia por la etiqueta linea que es como se nombra la variable
- En la consulta que hace a la base de datos no introduce la clausula ‘order by’ . Es decir es el propio hibernate el que encarga de ordenar los resultados. Tenerlo en cuenta cuando haya miles de registros devueltos.
Y así queda demostrado la importancia de establecer el tipo de unión pues si imaginamos una factura que tenga miles de líneas (improbable lo sé), si establecemos el método de unión a lazy en vez de hacer una sola consulta a la base de datos, haría 1000 lo cual, por supuesto ralentizaría muchísimo nuestra consulta, aparte de sobrecargar innecesariamente el servidor de la base de datos