Herramientas de usuario

Herramientas del sitio


unidades:05_hibernate_query_language:05_optimizacion

Diferencias

Muestra las diferencias entre dos versiones de la página.


unidades:05_hibernate_query_language:05_optimizacion [2023/04/07 21:26] (actual) – creado - editor externo 127.0.0.1
Línea 1: Línea 1:
 +====== 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:
 +
 +<uml>
 +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
 +</uml>
 +
 +==== Modelo de Tablas ====
 +El modelo de tablas es el siguiente:
 +
 +<uml>
 +class Profesor <<Table>>
 +Profesor : INTEGER id
 +Profesor : VARCHAR nombre
 +Profesor : VARCHAR ape1
 +Profesor : VARCHAR ape2
 +
 +
 +class CorreoElectronico <<Table>>
 +CorreoElectronico: INTEGER idCorreo
 +CorreoElectronico: VARCHAR direccionCorreo
 +CorreoElectronico: INTEGER idProfesor
 +
 +
 +Profesor "1" -- "0..n" CorreoElectronico
 +</uml>
 +
 +=====  =====
 +
 +
 +Veamos ahora dos optimizaciones que podemos realizar en hibernate:
 +  * [[#El problemas de las "n+1" SELECTs]]: Mejoraremos el rendimiento minimizando **cuántas SQL** lanza Hibernate.
 +  * [[#Consultas nativas]]: Mejoraremos el rendimiento ajustanto **qué SQLs** lanza 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.
 +
 +<note warning>Este es el mayor problema de rendimiento al que nos podemos enfrentar con Hibernate así que será necesario  tenerlo siempre en cuenta</note>
 +
 +Veamos ahora un ejemplo con Hibernate de este problema.
 +
 +<code java 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);
 +    }
 +}
 +</code>
 +
 +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:
 +<code sql>
 +SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos
 +</code>
 +
 +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:
 +<code java 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);
 +    }
 +}
 +</code>
 +
 +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.
 +<code sql>
 +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
 +</code>
 +
 +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 <javadoc jdk7>java.util.Set|Set</javadoc> de Java , ya que por definición no permite objetos duplicados, a diferencia de un <javadoc jdk7>java.util.List|List</javadoc>  que si que permite duplicados  y ya sin duplicados, volver a añadirlos a la lista.
 +
 +<code java 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);
 +    }
 +}
 +</code>
 +
 +Vemos que en la línea 4 se añaden los datos de la lista al <javadoc jdk7>java.util.Set|Set</javadoc> , 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 <javadoc jdk7>java.util.LinkedHashSet|LinkedHashSet</javadoc> en vez de un <javadoc jdk7>java.util.HashSet|HashSet</javadoc>. Simplemente porque el <javadoc jdk7>java.util.LinkedHashSet|LinkedHashSet</javadoc> 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:
 +  * [[https://community.jboss.org/wiki/HibernateFAQ-AdvancedProblems?_sscc=t#Hibernate_does_not_return_distinct_results_for_a_query_with_outer_join_fetching_enabled_for_a_collection_even_if_I_use_the_distinct_keyword|Hibernate does not return distinct results for a query with outer join fetching enabled for a collection (even if I use the distinct keyword)?]]
 +  * [[https://hibernate.atlassian.net/browse/HHH-1076|FETCH JOIN with DISTINCT returns duplicate values]]
 +  * [[http://www.javalobby.org/java/forums/t68781.html|How to remove duplicate objects returned in Hibernate when using HQL?]]
 +==== 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.
 +
 +<uml>
 +class Centro
 +class Aula
 +class Familia
 +class Ciclo
 +class Modulo
 +class Profesor
 +class Alumno
 +
 +Centro "1" -- "1..n" Aula
 +Centro "*" -- "1..n" Familia
 +Familia "1" -- "1..n" Ciclo
 +Familia "1" -- "1..n" Profesor
 +Ciclo "1" -- "1..n" Modulo
 +Modulo "0..n" -- "1..n" Profesor
 +Modulo "1..n" -- "0..n" Alumno
 +Centro "*" -- "1..n" Profesor
 +</uml>
 +
 +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 ''Centro''porque 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:
 +<code sql>
 +SELECT c FROM Centro c
 +</code>
 +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 <javadoc h41>org.hibernate.Session|Session</javadoc> contiene el método <javadoc h41>org.hibernate.SharedSessionContract#createSQLQuery(java.lang.String)|createSQLQuery(java.lang.String)</javadoc> que retorna un objeto <javadoc h41>org.hibernate.Query|Query</javadoc> pero a partir de un SQL nativa de la base de datos.
 +
 +La posterior llamada al método <javadoc h41>org.hibernate.Query#list()|list()</javadoc> retornará un ''List<Object[]>''.
 +
 +El siguiente ejemplo muestra cómo acceder a la **tabla** ''CicloFormativo''.
 +
 +<code java 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]);
 +}
 +</code>
 +
 +  * En la línea 1 vemos cómo se llama al método <javadoc h41>org.hibernate.SharedSessionContract#createSQLQuery(java.lang.String)|createSQLQuery(java.lang.String)</javadoc> con una SQL nativa.
 +  * El resto de las líneas son iguales a lanzar una consulta HQL.
 +
 +<note important>
 +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
 +</note>
 +
 +==== 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''.
 +<code xml 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>
 +</code>
 +
 +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:
 +<code java 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();
 +</code>
 +
 +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=?
 +
 +<note tip>
 +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 [[wp>Hint_(SQL)]] tal y como se explica para MySQL en [[http://dev.mysql.com/doc/refman/5.5/en/index-hints.html|Index Hint Syntax]] y para Oracle en [[http://docs.oracle.com/cd/E11882_01/server.112/e16638/hintsref.htm|Using Optimizer Hints]]
 +</note>
 +===== Otras optimizaciones =====
 +El tema de las optimizaciones en Hibernate es muy amplio pero no nos extenderemos más en el tema (( realmente Hibernate es muy amplio y en cada tema siempre hay que limitar lo que se explica )). Sin embargo debido a la importancia del rendimiento dejo unos enlaces con más información sobre el tema:
 +
 +  * [[http://docs.jboss.org/hibernate/orm/4.1/devguide/en-US/html/ch13.html|Native SQL Queries]]: Capítulo 13 del "Hibernate Developer Guide" sobre consultas nativas en SQL.
 +  *  [[http://docs.jboss.org/hibernate/orm/4.1/manual/en-US/html/ch21.html|Improving performance]]:Capítulo 21 del "Hibernate Reference Documentation" sobre optimización. Es especialmente importante lo relativo a //Fetching// y la cache