Tabla de Contenidos

Optimización

Uno de los mayores problemas que nos podemos encontrar al usar un ORM es la lentitud que puede tener respecto a una aplicación realizada directamente con JDBC.

Cuando realizamos una aplicación mediante JDBC podemos determinar el número de SQL que van a lanzarse y optimizar cada una de ellas. Sin embargo, desde Hibernate inicialmente no podemos hacerlo ya que es Hibernate el que se encarga de realizar todas las SQL por nosotros. El no controlar las SQL nos puede llevar a problemas de rendimiento en nuestra aplicación.

Por suerte para nosotros y gracias a la potencia de Hibernate podemos ajustar mucho cuántas SQLs y qué SQLs lanza Hibernate. El problema de ello es que deberemos conocer más profundamente cómo funciona Hibernate con el coste que ello lleva asociado.

Modelo

En los ejemplos que vamos a realizar van a usarse las siguientes clases Java y tablas, que sólo mostraremos en formato UML. No vamos a poner el código fuente ni los ficheros de hibernate de mapeo ya que aún no han sido explicadas en lecciones anteriores todas las características que usan.

Modelo de Java

El modelo de clases Java es el siguiente:


class Profesor
Profesor : int id
Profesor : String nombre
Profesor : String ape1
Profesor : String ape2


class CorreoElectronico
CorreoElectronico: int idCorreo
CorreoElectronico: String direccionCorreo


Profesor "1" -- "0..n" CorreoElectronico: correosElectronicos

Modelo de Tablas

El modelo de tablas es el siguiente:


class Profesor <>
Profesor : INTEGER id
Profesor : VARCHAR nombre
Profesor : VARCHAR ape1
Profesor : VARCHAR ape2


class CorreoElectronico <
>
CorreoElectronico: INTEGER idCorreo
CorreoElectronico: VARCHAR direccionCorreo
CorreoElectronico: INTEGER idProfesor


Profesor "1" -- "0..n" CorreoElectronico

Veamos ahora dos optimizaciones que podemos realizar en hibernate:

El problemas de las "n+1" SELECTs

Uno de los mayores problemas de los ORM es el problema de los “n+1” SELECTs. Este problema consiste en que al lanzar un consulta con HQL que retorna n filas, el ORM lanza n+1 consultas SQL de SELECT. Como podemos imaginar ésto genera unas perdidas de rendimiento brutales.

Este es el mayor problema de rendimiento al que nos podemos enfrentar con Hibernate así que será necesario tenerlo siempre en cuenta

Veamos ahora un ejemplo con Hibernate de este problema.

1
Query query = session.createQuery("SELECT p FROM Profesor p");
List<Profesor> profesores = query.list();
for (Profesor profesor : profesores) {
    System.out.println(profesor.toString());
    for (CorreoElectronico correoElectronico : profesor.getCorreosElectronicos()) {
        System.out.println("\t"+correoElectronico);
    }
}

Este código lo único que hace es mostrar todos los profesores y para cada profesor mostrar todas sus direcciones de correo. Ësto lo hemos realizado lanzado únicamente una consulta HQL contra Hibernate.

Al ejecutar el programa y comprobar las SELECTs de SQL que se han lanzado podemos ver coóo se lanza una primera consulta para obtener todos los profesores pero posteriormente se lanza una consulta adicional por cada profesor para obtener los correos electrónicos de cada profesor. Es decir que se ejecutan “n+1” SELECTs siendo “n” el número de filas que retorna la primera consulta, en nuestro caso el número de profesores.

Solucion con left join fetch

La solución más sencilla es modificar la consulta de HQL para que cargue también todos los correos electrónicos. Para ello deberemos hacer un LEFT JOIN FETCH entre los profesores y los correos electrónicos.

La consulta HQL que realiza el LEFT JOIN FETCH entre los profesores y los correos electrónicos es la siguiente:

SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos

Como vemos el LEFT JOIN FETCH se hace entre la tabla principal y la propiedad de la que queremos que se carguen todos los datos, en nuestro caso la propiedad Profesor.correosElectronicos.

El código Java en este caso queda de la siguiente forma:

1
Query query = session.createQuery("SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos");
List<Profesor> profesores = query.list();
for (Profesor profesor : profesores) {
    System.out.println(profesor.toString());
    for (CorreoElectronico correoElectronico : profesor.getCorreosElectronicos()) {
        System.out.println("\t"+correoElectronico);
    }
}

Vemos que sólo hemos modificado la línea 1 con la nueva consulta.

Si ejecutamos ahora el código podremos ver en la consola que se ha lanzado unicamente la siguiente SELECT de SQL, por lo que se ha solucionado el problema.

SELECT profesor0_.Id AS Id0_0_, correosele1_.IdCorreo AS IdCorreo1_1_, profesor0_.nombre AS nombre0_0_, profesor0_.ape1 AS ape3_0_0_, profesor0_.ape2 AS ape4_0_0_, correosele1_.direccionCorreo AS direccio2_1_1_, correosele1_.idProfesor AS idProfesor1_1_, correosele1_.idProfesor AS idProfesor0_0__, correosele1_.IdCorreo AS IdCorreo0__ FROM Profesor profesor0_ LEFT OUTER JOIN CorreoElectronico correosele1_ ON profesor0_.Id=correosele1_.idProfesor

En la SQL podemos ver como se realiza un Left Outer Join entre la tabla Profesor y la tabla CorreoElectronico

Desgraciadamente con ésto no es suficiente. Si ejecutamos el código veremos que se repiten los objetos Profesor. ¿Porqué? El lenguaje SQL no permite consultar jerárquicas , así que lo que hace es retornar todos los profesores con todos los correos , por lo que el mismo profesor está repetido tantas veces como correos. Hibernate desgraciadamente no elimina esa duplicidad así que debemos explicitamente desde Java eliminar los duplicados.

Para eliminar los objetos duplicados usaremos el truco de pasarlos todos a un Set de Java , ya que por definición no permite objetos duplicados, a diferencia de un List que si que permite duplicados y ya sin duplicados, volver a añadirlos a la lista.

1
Query query = session.createQuery("SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos");
List<Profesor> profesores = query.list();
 
Set<Profesor> profesoresSinDuplicar = new LinkedHashSet<Profesor>(profesores);
profesores.clear(); 
profesores.addAll(profesoresSinDuplicar);
 
for (Profesor profesor : profesores) {
    System.out.println(profesor.toString());
    for (CorreoElectronico correoElectronico : profesor.getCorreosElectronicos()) {
        System.out.println("\t"+correoElectronico);
    }
}

Vemos que en la línea 4 se añaden los datos de la lista al Set , el cual eliminará los duplicados, se borra la lista original en la línea 5 y finalmente en la línea 6 se vuelven a poner en la lista pero ya sin duplicados.

Por último queda explicar porqué usamos un LinkedHashSet en vez de un HashSet. Simplemente porque el LinkedHashSet nos garantiza que al obtener los datos (línea 6) estarán en el mismo orden en el que se insertaron lo que hará que se mantenga el mismo orden de la lista original, cosa muy necesaria si en la HQL se uso un ORDER BY.

Mas información en:

Solucion con Lazy Loading

Hasta ahora no hemos hablado del Lazy Loading ( o carga perezosa en castellano ). El Lazy Loading consiste en que cuando lanzamos una consulta HQL , Hibernate por defecto intenta cargar el mínimo de datos posible ya que quizás no los necesitemos todos y sea una perdida de recursos.

Debido a las relaciones que hay entre los distintos objetos el cargar un objeto Java podría implicar cargar cientos de objetos relacionados con él. Imaginemos el siguiente diagrama UML con relaciones entre clase Java.

CentroAulaFamiliaCicloModuloProfesorAlumno11..n*1..n11..n11..n11..n0..n1..n1..n0..n*1..n

Nos puede haber parecido erróneo en el ejemplo inicial que Hibernate al cargar un profesor no haya cargado también sus correos electrónicos, -l fin y al cabo son datos del profesor- pero siguiendo con esa lógica veamos que pasaría con el ejemplo que hemos mostrado en el diagrama UML.

  • Si cargamos un objeto Centroporque queremos saber simplemente el nombre del centro, deberían cargarse sus objetos Aula , Familia y Profesor
  • pero si hemos cargado algún objeto de Familia también habría que cargar los objetos de Ciclo
  • pero si hemos cargado algún objeto de Ciclo habría que cargar también los objetos de Modulo
  • pero si hemos cargado algún objeto de Modulo también habría que cargar los objetos de Alumno

Es decir que con una simple consulta HQL como la siguiente:

SELECT c FROM Centro c

Se cargaría toda la base de datos en memoria, cosa que también empeoraría el rendimiento de la aplicación al saturar la memoria del servidor de aplicaciones.

Así que Hibernate hace una carga perezosa ( Lazy Loading ) cargando los datos sólo a medida que los vamos necesitando, lo que nos llevaría al problema del n+1 SELECTs. Pero en este caso no sería sido un problema ya que solo queremos saber el nombre de los centros y NO todas las propiedades del centro como sus profesores, familias o aulas.

Resumiendo, el problema del n+1 SELECTs puede no ser un problema si no vamos a acceder a otros datos hijos de la clase que hemos solicitado. Por lo tanto Hibernate hace bien en no cargarlos todos y permitir al programador definir cómo se hace la carga.

Consultas nativas

Los creadores de Hibernate no han sido tan soberbios como para pensar que las consultas en SQL que lanza Hibernate son las mejores que se pueden hacer. Por ello hibernate permite que el usuario pueda lanzar directamente consultas SQL contra la base de datos y de esa forma hacerlo de la forma más optimizada.

createSQLQuery

La clase Session contiene el método createSQLQuery(java.lang.String) que retorna un objeto Query pero a partir de un SQL nativa de la base de datos.

La posterior llamada al método list() retornará un List<Object[]>.

El siguiente ejemplo muestra cómo acceder a la tabla CicloFormativo.

1
Query query = session.createSQLQuery("SELECT IdCiclo,nombreCiclo,Horas FROM CicloFormativo");
List<Object[]> listDatos = query.list();
 
for (Object[] datos : listDatos) {
    System.out.println(datos[0] + "-" + datos[1] + " " + datos[2]);
}
  • En la línea 1 vemos cómo se llama al método createSQLQuery(java.lang.String) con una SQL nativa.
  • El resto de las líneas son iguales a lanzar una consulta HQL.
Hay que fijarse que al ser una SQL ya se hace referencia a las tablas y columnas de la base de datos en vez de a las clases y propiedades Java

SQL Personalizadas

Hibernate también permite que nosotros especifiquemos las SQL que van a usarse cuando se realiza una inserción , actualización y borrado en caso de que las que usa Hibernate tuvieran algún problema.

El fichero de mapeo de Hibernate incluye los siguientes 3 nuevos tags para especificar las SQL

  • <sql-insert> : Contiene una sql de INSERT
  • <sql-update> : Contiene una sql de UPDATE
  • <sql-delete> : Contiene una sql de DELETE

El siguiente fichero Profesor.hbm.xml contiene un ejemplo para la clase Profesor.

1|Profesor.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="ejemplo05.Profesor" >
    <id column="Id" name="id" type="integer" >
        <generator class="increment" />
    </id>
    <property name="nombre" />
    <property name="ape1" />
    <property name="ape2" />
 
    <set name="correosElectronicos"  cascade="all" inverse="true"   >
        <key>
            <column name="idProfesor" />
        </key>            
        <one-to-many class="ejemplo05.CorreoElectronico" />
    </set>
 
    <sql-insert>INSERT INTO Profesor (Nombre,Ape1,Ape2,Id) VALUES (?,?,?,?)</sql-insert>
    <sql-update>UPDATE Profesor SET Nombre=?,Ape1=?,Ape2=? WHERE Id=? </sql-update>
    <sql-delete>DELETE FROM Profesor WHERE Id=?</sql-delete>
  </class>
</hibernate-mapping>

Vemos cómo en las líneas 19, 20 y 21 se han definido las 3 SQL. Nótese que al ser SQL Nativas se podría usar cualquier característica especifica que necesitemos de la base de datos que estemos usando.

Si ejecutamos el siguiente código Java podremos ver por la consola las 3 SQL que hemos definido:

1
System.out.println("----------- Consultas peronalizadas para INSERT,  UPDATE y DELETE -----------");
Profesor profesor;
 
session.beginTransaction();
    profesor=new Profesor("Celia", "Sanchez", "Jordá");
    session.save(profesor);
session.getTransaction().commit();
 
session.beginTransaction();
    profesor.setNombre("Juan Carlos");
    session.update(profesor);
session.getTransaction().commit();
 
session.beginTransaction();
    session.delete(profesor);
session.getTransaction().commit();

Como vemos en el código, se inserta una nueva fila en la línea 6 , en la línea 11 se actualiza y finalmente en la línea 15 se borra.Al ejecutarlo se muestra por consola lo siguiente:

Hibernate: select max(Id) from Profesor
Hibernate: INSERT INTO Profesor (Nombre,Ape1,Ape2,Id) VALUES (?,?,?,?)
Hibernate: UPDATE Profesor SET Nombre=?,Ape1=?,Ape2=? WHERE Id=?
Hibernate: DELETE FROM Profesor WHERE Id=?
El ejemplo que hemos puesto es muy naif pero es normal en modelos de tablas complejos tener que optimizar las SQLs mediante el uso de Hint_(SQL) tal y como se explica para MySQL en Index Hint Syntax y para Oracle en Using Optimizer Hints

Otras optimizaciones

El tema de las optimizaciones en Hibernate es muy amplio pero no nos extenderemos más en el tema 1). Sin embargo debido a la importancia del rendimiento dejo unos enlaces con más información sobre el tema:

  • Native SQL Queries: Capítulo 13 del “Hibernate Developer Guide” sobre consultas nativas en SQL.
  • Improving performance:Capítulo 21 del “Hibernate Reference Documentation” sobre optimización. Es especialmente importante lo relativo a Fetching y la cache
1)
realmente Hibernate es muy amplio y en cada tema siempre hay que limitar lo que se explica