====== 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]]: 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. 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. Query query = session.createQuery("SELECT p FROM Profesor p"); List 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: Query query = session.createQuery("SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos"); List 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 java.util.Set|Set de Java , ya que por definición no permite objetos duplicados, a diferencia de un java.util.List|List que si que permite duplicados y ya sin duplicados, volver a añadirlos a la lista. Query query = session.createQuery("SELECT p FROM Profesor p LEFT JOIN FETCH p.correosElectronicos"); List profesores = query.list(); Set profesoresSinDuplicar = new LinkedHashSet(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 java.util.Set|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 java.util.LinkedHashSet|LinkedHashSet en vez de un java.util.HashSet|HashSet. Simplemente porque el java.util.LinkedHashSet|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: * [[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. 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 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: 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 org.hibernate.Session|Session contiene el método org.hibernate.SharedSessionContract#createSQLQuery(java.lang.String)|createSQLQuery(java.lang.String) que retorna un objeto org.hibernate.Query|Query pero a partir de un SQL nativa de la base de datos. La posterior llamada al método org.hibernate.Query#list()|list() retornará un ''List''. El siguiente ejemplo muestra cómo acceder a la **tabla** ''CicloFormativo''. Query query = session.createSQLQuery("SELECT IdCiclo,nombreCiclo,Horas FROM CicloFormativo"); List 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 org.hibernate.SharedSessionContract#createSQLQuery(java.lang.String)|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 * '''' : Contiene una sql de ''INSERT'' * '''' : Contiene una sql de ''UPDATE'' * '''' : Contiene una sql de ''DELETE'' El siguiente fichero ''Profesor.hbm.xml'' contiene un ejemplo para la clase ''Profesor''. INSERT INTO Profesor (Nombre,Ape1,Ape2,Id) VALUES (?,?,?,?) UPDATE Profesor SET Nombre=?,Ape1=?,Ape2=? WHERE Id=? DELETE FROM Profesor WHERE Id=? 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: 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 [[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]] ===== 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