Skip to content

8 Hibernate

Introducción

Hibernate es un proveedor de JPA, uno de los más populares y ampliamente utilizados en la comunidad de desarrollo de Java. Actúa como una implementación de la especificación JPA, permitiendo a los desarrolladores interactuar con bases de datos relacionales utilizando entidades mapeadas en objetos Java.

Hibernate ofrece una serie de características poderosas para el mapeo objeto-relacional, incluyendo:

  1. Mapeo de entidades: Hibernate permite mapear clases Java a tablas en la base de datos y atributos de clase a columnas en esas tablas.
  2. Consultas JPQL: Proporciona un lenguaje de consulta llamado JPQL (Java Persistence Query Language) que permite realizar consultas orientadas a objetos en lugar de escribir SQL directamente.
  3. Gestión de transacciones: Hibernate facilita la gestión de transacciones en aplicaciones JPA, lo que garantiza la consistencia y la integridad de los datos.
  4. Caché de primer y segundo nivel: Ofrece mecanismos de caché que pueden mejorar el rendimiento de las consultas y reducir la carga en la base de datos.
  5. Relaciones: Hibernate admite una variedad de tipos de relaciones, incluyendo relaciones uno a uno, uno a muchos y muchos a muchos, lo que facilita el modelado de datos complejos.
  6. Herencia: Permite mapear jerarquías de clases y herencia de manera eficiente.
  7. Integración con Java EE y Spring: Hibernate se puede integrar con entornos de desarrollo empresarial como Java EE y marcos de desarrollo como Spring.

Es importante destacar que Hibernate no es el único proveedor de JPA disponible. Otros proveedores, como EclipseLink y Apache OpenJPA, también implementan la especificación JPA y proporcionan funcionalidades similares. La elección de un proveedor específico depende de los requisitos del proyecto y las preferencias del desarrollador. Hibernate es popular debido a su amplia comunidad de usuarios, su amplia adopción y su rico conjunto de características.

Arquitectura Hibernate

Hibernate parte de la filosofía de mapear objetos Java (POJO). Para almacenar y recuperar estos objetos de la base de datos, el desarrollador debe mantener una conversación con el motor de Hibernate mediante un objeto especial que es la sesión (equiparable al concepto de conexión de JDBC). Igual que con las conexiones JDBC, las sesiones se deben crear y cerrar.

Arquitectura Hibernate

Figura 6 - Arquitectura hibernate

La clase Session pertenece al paquete org.hibernate.Session y ofrece métodos como save(Object object), createQuery(String sql), beginTransaction(), close(), etc, para interactuar con la base de datos.

Por ejemplo, podemos insertar un objeto con save(Object o) sin necesidad de especificar una sentencia SQL. Una instancia de Session no consume mucha memoria y su creación y destrucción es muy eficiente, lo que es muy importante ya que podemos crear y destruir sesiones todo el tiempo, quizás en cada petición.

Las interfaces de hibernate son las siguientes:

  • La interfaz SessionFactory permite obtener instancias Session. Esta interfaz debe compartirse entre muchos hilos de ejecución. Por regla general hay una única SessionFactory para toda la aplicación, creada durante la inicialización de la misma, y se utilizar para crear todas las sesiones relacionadas con un contexto dado. Si la aplicación accede a varias bases de datos se necesitará una SessionFactory por cada base de datos.
  • La interfaz Configuration se utiliza para configurar Hibernate. La aplicación la utiliza para especificar la ubicación de los documentos que indican el mapeado de los objetos y propiedades especificas de Hibernate, y a continuación crea la SessionFactory.
  • La interfaz Query permite realizar consultar a la base de datos y controla cómo se ejecutan dichas consultas. Las consultas se escriben en HQL o en el dialecto SQL nativo de la base de datos que estemos utilizando. Una instancia de esta interfaz se utiliza para enlazar los parámetros de la consulta, limitar el número de resultados devueltos y para ejecutar dicha consulta.
  • La interfaz Transaction nos permite asegurar que cualquier error que ocurra entre el inicio y el final de la transacción produzca el fallo de la misma.

    flowchart
        cfg["Configuration
        Configuration cfg = new Configuration().configure();"]
        factory["SessionFactory
        SessionFactory sessionFactory = cfg.buildSessionFactory();"]
        db[(Database)]
        session1["Session
        Session session = sessionFactory.openSession();"]
        session2["Session
        Session session = sessionFactory.openSession();"]
    
        subgraph one
        trans1["Transaction
        Transaction tx = session.beginTransaction"]
        subgraph two LR
        save1["save"]
        load1["load"]
        delete1["delete"]
        get1["get"]
        update1["update"]
        end
        subgraph three LR 
        commit1["tx.commit();"]
        rollback1["tx.rollback();"]
        end
        close1["session.close();"]
        end
    
        subgraph four
        trans1["**Transaction**
        Transaction tx = session.beginTransaction"]
        subgraph five LR
        save2["save"]
        load2["load"]
        delete2["delete"]
        get2["get"]
        update2["update"]
        end
        subgraph six LR 
        commit2["tx.commit();"]
        rollback2["tx.rollback();"]
        end
        close2["session.close();"]
        end
    
        cfg --> factory
        factory --> session1
        factory --> session2
        factory --> db
        session1 --> one
        session2 --> four
    Figura 7 - Aplicación con Hibernate

Configuración

El fichero de configuración de Hibernate es un archivo XML que contiene una serie de elemento que indican sus propiedad, como el protocolo a seguir, y los datos de conexión.

El fichero de configuración debe ir en la raíz del proyecto o en la carpeta resources si se usa un gestor de proyectos. El nombre del fichero debe ser hibernate.cfg.xml

<hibernate-configuration>
    <session-factory name="MyConnection">
        <property name="hibernate.connection.drive_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password"></property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost/horario</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    </session-factory>
</hibernate-configuration>

En el ejemplo anterior vemos como tenemos una factoría de sesión llama MyConnection con las propiedades pertinentes para una conexión a la base de datos horario.

Este archivo de configuración servirá como sustituto del archivo de configuración de la JPA estándar (persistence.xml).

Para poder indicar el mapeo de una entidad, se usará la etiqueta <mapping>. Se utilizará junto el atributo class cuando el mapeo de una entidad viene determinado por una clase, mientras que con el atributo resource se indicará el archivo ORM que define el mapeo.

<mapping class="com.package.CLASS"/>

<mapping resource="url/to/ORM.xml"/>

Importante

Las clases que funcione como entidades deben implementar la interfaz Serializable. Además se recomiendan los estándares de renombrado de JavaBean.

Archivos de mapeo

A la hora de realizar el mapeo de una entidad a través de anotaciones, no es necesario realizar ningún cambio, ya que Hibernate usa las anotaciones del paquete de JPA. Mientras que si queremos mapear una entidad a través un archivo XML, podemos usar la nomenclatura estándar ed la JPA o una específica para Hibernate (recomendado).

Un ejemplo de una configuración específica es:

<?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="ejemplo01.Profesor" table="Profesor" >
    <id column="Id" name="id" type="integer">
        <generator class="identity"/>
    </id>
    <property name="nombre" />
    <property name="ape1" />
    <property name="ape2" />
  </class>
</hibernate-mapping>

De este ejemplo podemos destacar las siguientes etiqueta:

  • class: indica la clase que hace de entidad. Con el atributo name indicamos la ruta de paquetes del fichero Java y con el atributo table indica el nombre de la tabla de la base de datos con la que está relacionada.
  • id: indica el atributo de la clase que actúa como primary key. Con el atributo column indicamos el nombre de la columna de la base de datos a la que hace referencia, y con el atributo name el nombre del atributo de la clase. Además con type podemos indicar el tipo de dato.
  • property: indica los atributos de las clases. También tiene el atributo access al igual que en los archivos ORM estándar pero en esta ocasión se usa por defecto el valor property
  • generator: indica la forma en la que se va a generar la clave primaria. Los valores posibles son:
    • increment: genera identificadores.
    • identity: admite columnas de identidad de DB2, MySQL, MS SQL Server, Sybase y HypersonicSQL.
    • sequence: usa una secuencia en DB2, PostgreSQL, Oracle, SAP DB, McKoi o un generador en Interbase.

Info

Para más información de las etiquetas y de los atributos visite: http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd

Para las relaciones tenemos las siguientes etiquetas:

  • many-to-one: indica una relación de muchos a uno. Algunos de sus atributos más comunes son: name, column, class.
  • one-to-one: indica una relación de uno a uno.
  • join: mapea las propiedades de una clase a varias tablas que tengan relación uno-a-uno.

Para poder indicar una relación many-to-many hacemos uso de las etiquetas set, list o map. Los atributos más comunes son:

  • name: indica el nombre del atributo de la clase que representa la lista.
  • table: indica el nombre de la tabla que representa la relación. Por ejemplo, si tenemos una tabla product y otra customer, la tabla que representa la relación puede ser cart.

Dentro de estas etiquetas podemos encontrar otras como:

  • key: indica la clave de la relación, normalmente representa a la clave primaria de la entidad. En su interior se debe usar la etiqueta column para indicar a que columna hace referencia.
  • many-to-many: indica la otra clase identidad que debe mapear, si la relación es many to many.
  • one-to-many: indica la clase a la que hace referencia, pero en esta ocasión la relación es one to many.

Este ejemplo trata dos entidad, Stock y StockDailyRecord, cuya relación es de uno a muchos. La clase Stock representa el stock de un producto, mientras que StockDailyRecord representa los cambios de stock diarios:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.example.stock.Stock" table="stock" catalog="example_db">
        <id name="stockId" type="java.lang.Integer">
            <column name="STOCK_ID" />
            <generator class="identity" />
        </id>
        <property name="stockCode" type="string">
            <column name="STOCK_CODE" length="10" not-null="true" unique="true" />
        </property>
        <property name="stockName" type="string">
            <column name="STOCK_NAME" length="20" not-null="true" unique="true" />
        </property>
        <set name="stockDailyRecords" table="stock_daily_record"
    inverse="true" lazy="true" fetch="select">
            <key>
                <column name="STOCK_ID" not-null="true" />
            </key>
            <one-to-many class="com.example.stock.StockDailyRecord" />
        </set>
    </class>
</hibernate-mapping>
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.example.stock.StockDailyRecord" table="stock_daily_record" 
  catalog="example_db">
        <id name="recordId" type="java.lang.Integer">
            <column name="RECORD_ID" />
            <generator class="identity" />
        </id>
        <many-to-one name="stock" class="com.example.stock.Stock" fetch="select">
            <column name="STOCK_ID" not-null="true" />
        </many-to-one>
        <property name="priceOpen" type="java.lang.Float">
            <column name="PRICE_OPEN" precision="6" />
        </property>
        <property name="priceClose" type="java.lang.Float">
            <column name="PRICE_CLOSE" precision="6" />
        </property>
        <property name="priceChange" type="java.lang.Float">
            <column name="PRICE_CHANGE" precision="6" />
        </property>
        <property name="volume" type="java.lang.Long">
            <column name="VOLUME" />
        </property>
        <property name="date" type="date">
            <column name="DATE" length="10" not-null="true" unique="true" />
        </property>
    </class>
</hibernate-mapping>

Sessions y objetos hibernate

Para trabajar con hibernate se recomienda tener una clase que se encargue de crear una SessionFactory, se recomienda hacerlo con un patrón Singleton:

public class HibernateUtil{
    private static final SessionFactory sessionFactory = buildSessionFactory();

    private static SessionFactory buildSessionFactory(){
        try{
            Configuration configuration = new Configuration().configure()
            return configuration.buildSessionFactory(new StandardServiceRegistryBuilder().configure().build());
        } catch (Throwable ex){
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory(){
        return sessionFactory;
    }
}

Para poder obtener la sesión actual, será necesario cargar el fichero de configuración hibernate.cfg.xml. Para ello se llama a new Configuration().configure(), y de esta forma se inicializa el entorno de Hibernate. Se necesita crear un objeto StandardServiceRegistryque contiene una lista de servicios que utiliza Hibernate para crear el SessionFactory.

Transacciones

Un objeto Session de Hibernate representa una única unidad de trabajo para un almacén de datos dado y lo abre un ejemplar de SessionFactory (openSession()). Al crear la sesión se crea la transacción para dicha sesión (close()). Se deben cerrar las sesiones cuando se haya completado todo el trabajo de una transacción:

// Obtener la sesión
SessionFactory factory = HibernateUtil.getSessionFactory();
// Crear la sesión
Session session = factory.openSession();
// Crear transacción
Transaction tx = session.beginTransaction();

// Operaciones

tx.commit();
session.close(); // Se cierra la sesión

El método beginTransaction() marca el comienzo de una transacción y el método commitvalida una transacción. Mientras que el método, rollback() deshace la transacción.

Estados de un objeto Hibernate

Hibernate define y soporta los siguientes estados de objeto:

  • Transitorio (Transient): Un objeto es transitorio si ha sido recién instanciando utilizando el operador new, y no está asociado a una sesión de Hibernate- No tiene una representación persistente en la base de datos y no se le ha asignado un valor identificador. Las instancias transitorias serán destruidas por el recolector de basura si la aplicación no mantiene más de una referencia. Utiliza la sesión de Hibernate para hacer un objeto persistente y deja que Hibernate se ocupe de las declaraciones SQL que necesitan ejecutarse para esta transición. Las instancias recién instanciadas de una clase persistente, Hibernate las considera como transitorias. Podemos hacer una instancia transitoria persistente asociándola con una sesión.

    Department dep = new Department();
    dep.setDeptNo((byte) 60);
    dep.setDName("MARKETING");
    dep.setLoc("GUADALAJARA");
    session.save(dep); // (1)!
    
    1. save() hace que la instancia sea persistente
  • Persistente (Persistent). Un objeto estará en este estado cuando ya está almacenado en la base de datos. Puede haber sido guardado o cargado, sin embargo, por definición, se encuentra en el ámbito de una sesión. Hibernate detectará cualquier cambio realizado a un objeto en estado persistente y sincronizará el estado con la base de datos cuando se complete la unidad de trabajo. En definitiva, los objetos transitorios solo existen en memoria y no en un almacén de datos, han ido instanciados por el desarrollador sin haberlos almacenado mediante una sesión. Los persistentes se caracterizan por haber sido ya creados y almacenados en una sesión o bien devueltos en una consulta realizada con la sesión.

  • Separado (Detached). Un objeto está en este estado cuando cerramos la sesión mediante el método close(). Una instancia separada es un objeto que se ha hecho persistente, pero su sesión ha sido cerrada. La referencia al objeto todavía es válida, por supuesto, y la instancia separada podría incluso ser modificada en este estado. Una instancia separada puede ser asociada a una nueva sesión más tarde, haciéndola persistente de nuevo (con todas las modificaciones).

Carga de objetos

Para cargar objetos usaremos lo siguientes métodos de la clase Session:

Método Descripción
<T> T load(Class<T> clase, Serializable id) Devuelve la instancia persistente de la clase indicada con el identificador dado. La instancia tiene que existir, si no existe el método lanza una excepción ObjectNotFound.
Object load(String className, Serializable id) Similar al método anterior, pero en este caso indicamos en el primer parámetro el nombre de la case en formato String.
<T> T get(Class<T> class, Serializable id) Devuelve la instancia persistente de la clase indicada con el identificador dado. Si la instancia no existe, devuelve null.
Object get(String className, Serializable id) Similar al método anterior, pero en este caso indicamos en el primer parámetro el nombre de la clase

Si queremos obtener un objeto y que se lance una clase en caso de que no exista, se usaría el método load():

try{
    Department dep = (Department) session.load(Department.class, (byte) 20);
    // ...
} class (ObjectNotFoundException o){
    // ...
}

Por el contrario, si no queremos que lance una excepción usaremos el método get():

Department dep = (Department) session.get(Department.class, (byte) 11);

if(dep == null){
    System.out.println("No existe el departamento con id " + 11);
} else {
    //...
}

Operaciones básicas de objetos

Para almacenamiento, modificación y borrado de objetos usamos los siguiente métodos:

Métodos Descripción
Serializable persist(Object o) Guarda el objeto que se pasa como argumento en la base de datos. Hace que la instancia transitoria del objeto sea persistente.
void merge(Object o) Actualiza en la base de datos el objeto que se pasa como argumento. El objeto a modificar debe ser cargado con el método load() o get().
void remove(Object o) Elimina de la base de datos el objeto que se pasa como argumento. El Objeto a eliminar debe ser cargado con el método load() o get().

Al insertar un objeto puede producirse una excepción en caso de que se cree un objeto que necesite de otro, como Inscription necesita un estudiante y un curso (TransientPropertyValueException, al hacer el método persist())

Además dichas operaciones puede dar una excepción en caso de que se intente actualizar o eliminar un objeto que no exista en la base de datos, o se intente almacenar un objeto ya existente (ConstraintViolationException). Esta excepción se lanza en el método commit().

SessionFactory factory = HibernateUtil.getSessionFactory();
Session session = factory.openSession();
Transaction tx = session.beginTransaction();

try{
    Department dep = new Department();

    dep.setDeptNo((byte) 70);
    dep.setDName("INFORMÁTICA");
    dep.setLoc("TOLEDO");

    session.save(dep);
    tx.commit();
} catch(ConstraintViolationException e){
    //...
}
session.close();
SessionFactory factory = HibernateUtil.getSessionFactory();
Session session = factory.openSession();
Transaction tx = session.beginTransaction();

try{
    Employee em = (Employee) session.load(Employee.class, (short) 7369);

    em.setSalary(em.getSalary() + 1000);

    session.update(em);
    tx.commit();
} catch(ObjectNotFoundException o){
    // ...
} catch(ConstraintViolationException c){
    // ...
}
session.close();
SessionFactory factory = HibernateUtil.getSessionFactory();
Session session = factory.openSession();
Transaction tx = session.beginTransaction();

try{
    Employee em = (Employee) session.load(Employee.class, (short) 7369);

    session.delete(em);
    tx.commit();
} catch(ObjectNotFoundException o){
    // ...
} catch(ConstraintViolationException c){
    // ...
}

session.close();

Consultas

Hibernate soporta un lenguaje de consulta orientado a objetos denominado HQL (Hibernate Query Language) fácil de usar pero potente a la vez. Este lenguaje es una extensión orientada a objetos de SQL. Las consultas HQL y SQL nativas son representadas con una instancia de la interfaz Query (del paquete org.hibernate). Esta interfaz ofrece métodos para ligar parámetros, manejo del conjunto resultado, y para la ejecución de la consulta real. Siempre obtiene una Query utilizando el objeto Session actual.

Para crear una query se utilizaría el método createQuery(String sql) de la interfaz SharedSessionContract que implementa la Session:

Query q = session.createQuery("from Department");

Algunos métodos importantes de esta interfaz son:

Método Descripción
Iterator iterate() Devuelve en un objeto Iterator el resultado de la consulta.
List list() Devuelve el resultado de la consulta en un List
Query setFetchSize(int size) Fija el número de resultados a recuperar en cada acceso a la base de datos al valor indicado en size.
int executeUpdate() Ejecuta la sentencia de modificación o borrado. Devuelve el número de entidades afectadas
String getQueryString() Devuelva la consulta en un String
Object uniqueResult() Devuelve un objeto (cuando sabemos que la consulta devuelve un objeto) o nulo si la consulta no devuelve resultados
Query setCharacter(int position, char value)
Query setCharacter(String name, char value)
Asigna el value indicado en el método a un parámetro de tipo char.
position indica la posición del parámetro dentro de la consulta, empieza en 0.
name es el nombre (se indica :name) del parámetro dentro de la consulta
Query setDate(int position, Date value)
Query setDate(String name, Date value)
Asigna un value a un parámetro de tipo Date.
Query setDouble(int position, double value)
Query setDouble(String name, double value)
Asigna value a un parámetro de tipo decimal.
Query setInteger(int position, int value)
Query setInteger(String name, int value)
Asigna value a uin parámetro de tipo entero
Query setString(int position, String value)
Query setString(String name, String value)
Asigna un value a un parámetro de tipo VARCHAR
Query setParameterList(String name, Collection values) Asigna una colección de valores al parámetro cuyo nombre se indica en name
Query setParameter(String name, Object value) Asigna un valor al parámetro indicado en name

HQL

Hibernate Query Language (HQL) es un lenguaje bastante similar a SQL, pero en lugar de tablas se trata de clases y en lugar de columnas se trata de propiedades o atributos de una clase.

Las consultas HQL son consultas independientes de la base de datos porque las consultas HQL se convierten internamente en consultas SQL específicas de la base de datos utilizando la clase Dialect mencionada en el archivo hibernate-cfg.xml.

Usar este lenguaje puede tener las siguientes ventajas:

  1. Independiente de la base de datos
  2. Fácil de aprender para el programador Java
  3. HQL soporta totalmente las consultas polimórficas. Es decir, junto con el objeto que se devuelve como resultado de una consulta, se devolverán todos los objetos secundarios (objetos de subclases) del objeto dado. Se tiene todo el modelo.

Las siguientes clausulas a usar en HQL son:

Clausula Ejemplo Descripción
FROM FROM Employee Devuelve el objeto completo que representa la clase
AS FROM Employee AS E Indica un alias para acceder de forma sencilla a las propiedades de la clase (se puede omitir e indicar directamente el alias)
SELECT SELECT e.firstName FROM Employee as e Devuelve las propiedades de la clase específicas del objeto
WHERE FROM Employee AS E WHERE E.firstName like 'A%' Indica una condición para filtrar el conjunto de objetos.
ORDER BY FROM Person P WHERE P.id > 10 ORDER BY P.salary DESC Se indica para ordenar la consulta. Puede ir seguido de ASC si se desea un orden ascendente (se puede omitir ya que es el valor por defecto) o DESC si, por el contrario, se quiere un orden descendente
GROUP BY SELECT SUM(P.salary), P.firstName FROM Person P GROUP BY P.firstName Se usa para agrupar los datos de una consulta. Normalmente se usa para añadir un valor agregado.
INSERT INSERT INTO Employee (Salary, Loc) VALUES (1000, 'Cádiz') Crea objetos de una entidad
UPDATE UPDATE Employee E SET E.salary = E.salary * 1000 WHERE E.ID = 10 Actualiza los objetos
DELETE DELETE Employee E WHERE E.ID = 10 Elimina objetos

Se puede utilizar diferentes entidades separadas por coma , lo que producirá un cross join: from Department d, Employee e.

Las funciones de agregación, que normalmente van con la clausula GROUP BY, son:

  • COUNT: Se utiliza para contar el número de objetos resultantes: SELECT COUNT(*) FROM Employee E GROUP BY E.Loc.
  • SUM: Suma los valores de una propiedad específica: SELECT SUM(E.salary) FROM Employee E GROUP BY E.Loc.
  • AVG: Calcula el valor promedio de una columna específica: SELECT AVG(E.salary) FROM Employee E GROUP BY E.Loc.
  • MIN/MAX: Encuentra el valor mínimo/máximo: SELECT MIN(E.salary) FROM Employee E.
  • COALESCE: Comprueba si el primer valor indicado es null, en caso de serlo, comprueba el segundo valor, y sí este lo es, devuelve el siguiente, así sucesivamente. Por ejemplo, en la expresión coalesce(e.salary, null, 0), comprueba si salary es null, si no lo es devuelve su valor, y en caso de serlo comprueba el siguiente valor, que si es null, comprueba el siguiente valor, hasta encontrar uno de no es null, como en este caso, que si salary es null, devolverá 0, ya que el segundo argumento es null.

HQL también admite las consultas JOIN con las clausulas INNER JOIN, LEFT JOIN, RIGHT JOIN, FETCH JOIN, etc.

En el siguiente ejemplo se va a recorrer a todos los empleados que tengan un salario mayor al indicado como parámetro:

String hql = "FROM Employee e where e.salary > :salary";
Query<Employee> = session.createQuery(hql, Employee.class);
query.setInteger("salary", 1000);
List<Employee> = query.list();

Consultas con parámetros

Hibernate soporta parámetros con nombres y parámetros de estilo JDBC (?) en las consultas HQL. Los parámetros son enumerados desde 0, es decir, el primer parámetro que aparece estará en la posición 0, el siguiente en el 1, así sucesivamente. El uso de parámetros nombrados tiene una serie de ventajas:

  • son insensibles al orden en que aparecen en la cadena de consulta.
  • pueden aparecer múltiples veces en la misma petición.
  • son autodocumentados.

Para asignar valores a los parámetros se utilizan los métodos setXXX, vistos en la anterior tabla., siendo setParameter() la sintaxis simple.

String hql = "from Employees emp where emp.department.deptNo = ? and emp.oficio = ?";

Query q = session.createQuery(hql);
q.setParameter(0, (byte) 10);
q.setParameter(1, "Director");

El ejemplo anterior se puede hacer con parámetros nombrados y con sus métodos específicos para cada dato:

String hql = "from Employees emp where emp.department.deptNo = :ndep and emp.oficio = :ofi";

Query q = session.createQuery(hql);
q.setInteger('ndep', (byte) 10);
q.setString('ofi', "Director");

Incluso podemos pasarle una lista de parámetros:

List<Byte> numbers = new ArrayList<>();
numbers.add((byte) 20);
numbers.add((byte) 20);

String hql = "from Employees emp where emp.department.deptNo in (:listaDep) order by emp.department.deptNo";

Query q = session.createQuery(hql);
q.setParameterList("listDep", numbers);

Consultas sobre clases no asociadas

Si queremos recuperar los datos de una consulta en la que intervienen varias tablas y los atributos no están asociados a ninguna clase podemos utilizar la clase object. Los resultados se reciben en un array de objetos, donde el primer elemento del array se corresponde con la primera clase que ponemos a la derecha de FROM, el siguiente elemento con la siguiente clase, y así sucesivamente.

String hql = "from Employee e, Department d where e.department.deptNo = d.deptNo order by apellido"

Query q = session.createQuery(sql);
Iterator iterate = q.iterate();
Object[] par;
Employee employee;
Department department;

while(q.hasNext()){
    par = (Object[]) iterate.next();
    employee = (Employee) par[0];
    department = (Department) par[1];
    // ...
}

Supongamos, ahora, que se desea obtener el nombre de departamento, su número de empleados y el salario medio. Como los datos de esta consulta no están asociados a ninguna clase, se puede crear una y utilizarla sin la necesidad de mapearla.

public class Totales{
    private Long nEmployees;
    private Byte nDepartment;
    private Double avgSalary;
    private String departmentName;

    // Constructores y getter y setters
}
String hql = "select new package.Totales(" 
+ " d.deptNo, count(e.empNo), coalasce(ave(e.salario), 0)), d.nombre) "
+ " from Employees as e right join e.departments as d "
+ " group by d.deptNo, d.nombre";

Query q = session.createQuery(hql);
Iterator i = q.iterate();
Totales t;

while(t.hasNext()){
    t = (Totales) q.next();
    System.out.println(t);
}

Records

En Hibernate, no es posible utilizar records como clases de identidad, ya que es necesario tener los métodos setters para la modificación de la información que obtenemos de la base de datos. Sin embargo, a través del lenguaje JPQL o HPL, podemos obtener una consulta y envolverla en una clase record.

TypedQuery<BookWithAuthorNamesRecord> q = 
        em.createQuery(
                "SELECT new org.thoughts.on.java.model.BookWithAuthorNamesRecord("
                        + "b.id, b.title, b.price, concat(a.firstName, ' ', a.lastName)) "
                + " FROM Book b JOIN b.author a "
                + " WHERE b.title LIKE :title",
                BookWithAuthorNamesRecord.class);
q.setParameter("title", "%Hibernate Tips%");
List<BookWithAuthorNamesRecord> books = q.getResultList();

Otra solución es en la clase de entidad, indicarle que al realizar la consulta, mapee el resultado a la clase record deseada:

@SqlResultSetMapping(
        name = "BookWithAuthorNamesRecordMapping",
        classes = @ConstructorResult(
                targetClass = BookWithAuthorNamesRecord.class,
                columns = { @ColumnResult(name = "id", type = Long.class), 
                            @ColumnResult(name = "title"), 
                            @ColumnResult(name = "price"), 
                            @ColumnResult(name = "authorName")}))

Proyecto Students con Hibernate

Vamos ahora a modificar nuestro proyecto Students para añadirle todo lo aprendido. En primer lugar, eliminamos el persistence.xml y añadimos un fichero hibernate.cfg.xml:

<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory name="">
        <property name="hibernate.connection.drive_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password"></property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost/students</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>

        <mapping resource="orm/Course.xml"/>
        <mapping resource="orm/Inscription.xml"/>
        <mapping resource="orm/Student.xml"/>
    </session-factory>
</hibernate-configuration>

Luego creamos los ficheros xml de mapeo (en caso de usar anotaciones, no se deben modificar):

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.irudev.data.entity.Course" table="course">
        <id name="id" type="java.lang.Integer">
            <column name="id"/>
            <generator class="identity"/>
        </id>
        <property name="name" type="string">
            <column name="name"/>
        </property>
        <property name="credits" type="int">
            <column name="credits"/>
        </property>
    </class>
</hibernate-mapping>
<!-- estudiante.orm.xml -->
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.irudev.data.entity.Student" table="student">
        <id name="id" type="java.lang.Integer">
            <column name="id"/>
            <generator class="identity"/>
        </id>
        <property name="name" type="string">
            <column name="name" />
        </property>
        <property name="age" type="int">
            <column name="age"/>
        </property>
    </class>
</hibernate-mapping>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.irudev.data.entity.Inscription" table="inscription">
        <id name="id" type="java.lang.Long">
            <column name="id"/>
            <generator class="identity"/>
        </id>
        <many-to-one name="student" class="com.irudev.data.entity.Student">
            <column name="idStudent"/>
        </many-to-one>
        <many-to-one name="course" class="com.irudev.data.entity.Course">
            <column name="idCourse"/>
        </many-to-one>
    </class>
</hibernate-mapping>

Añadimos nuestro Singleton que controle nuestro acceso a las sesiones de Hibernate:

public class HibernateUtil {
    private static HibernateUtil instance;
    private SessionFactory factory;
    private Session session;

    private HibernateUtil(){}

    private void buildSessionFactory(){
        Configuration configuration = new Configuration().configure();
        StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
        factory = configuration.buildSessionFactory(registry);
    }

    private void openSession(){
        if(factory == null){
            buildSessionFactory();
        }

        if(session == null || !session.isOpen()){
            session = factory.openSession();
        }
    }

    public void closeSession(){
        if(session != null){
            session.close();
        }

        if(factory != null){
            factory.close();
        }
    }

    public Session getCurrentSession(){
        if(session == null){
            openSession();
        }

        return session;
    }

    public static HibernateUtil getInstance(){
        if(instance == null){
            instance = new HibernateUtil();
        }

        return instance;
    }
}

Por último, modificamos nuestros modelos:

public class JDBCCourseRepository implements CourseRepository {
    private final HibernateUtil manager;

    public JDBCCourseRepository(HibernateUtil manager) {
        this.manager = manager;
    }

    @Override
    public Course getById(Integer id) {
        return manager.getCurrentSession().get(Course.class, id);
    }

    @Override
    public List<Course> getAll() {
        return manager.getCurrentSession().createQuery("from Course c", Course.class).list();
    }


    @Override
    public boolean save(Course course) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().persist(course);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    @Override
    public boolean update(Course course) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().merge(course);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    @Override
    public boolean delete(Course course) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().remove(course);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }
}
public class JDBCStudentRepository implements StudentRepository {

    private final HibernateUtil manager;

    public JDBCStudentRepository(HibernateUtil manager){
        this.manager = manager;
    }

    @Override
    public Student getById(Integer id) {
        return manager.getCurrentSession().get(Student.class, id);
    }

    @Override
    public List<Student> getAll() {
        return manager.getCurrentSession().createQuery("from Student s", Student.class).list();
    }

    @Override
    public boolean save(Student student) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().persist(student);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    @Override
    public boolean update(Student student) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().merge(student);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    @Override
    public boolean delete(Student student) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().remove(student);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }
}
public class JDBCInscriptionRepository implements InscriptionRepository {
    private final HibernateUtil manager;

    public JDBCInscriptionRepository(HibernateUtil manager) {
        this.manager = manager;
    }

    @Override
    public Inscription getById(Integer id) {
        return manager.getCurrentSession().get(Inscription.class, id);
    }

    @Override
    public List<Inscription> getAll() {
        return manager.getCurrentSession().createQuery("from Inscription i", Inscription.class).list();

    }

    @Override
    public boolean save(Inscription inscription) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().persist(inscription);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }


    @Override
    public boolean delete(Inscription inscription) {
        try{
            Transaction tx = manager.getCurrentSession().beginTransaction();
            manager.getCurrentSession().remove(inscription);
            tx.commit();
            return true;
        }catch (Exception e){
            return false;
        }
    }
}