La persistencia de objetos
Al desarrollar una aplicación con el modelo de objetos, todas las entidades viven en la memoria principal. Y la memoria principal es limitada, además de que se borra al cerrar el programa. Para poder recuperar los objetos en cualquier momento futuro, independientemente de la ejecución, hace falta guardarlos en otro medio: a eso se lo llama persistencia. Es la misma idea de la lección 4, pero con objetos enteros en lugar de texto.
Y ahí aparece el problema de fondo, que es el que justifica toda esta lección: el mundo relacional y el mundo orientado a objetos no son del todo compatibles.
Para resolver ese problema existen frameworks que se encargan de hacer ese mapeo de objeto a relacional y de relacional a objeto. Se los llama ORM (Object-Relational Mapping, mapeo objeto-relacional).
Su principal ventaja es la reutilización: se pueden llamar a los métodos de un objeto de datos desde varias partes de la aplicación e incluso desde aplicaciones diferentes.
El otro problema que resuelve
Hay una consideración que aparece al construir cualquier capa de acceso a datos: las empresas que crean las bases de datos usan variantes diferentes de SQL. Si se cambia de gestor, hay que reescribir parte de las consultas que se habían definido para el anterior.
La solución es la misma idea de JDBC, llevada a las consultas: si se escriben con una sintaxis independiente de la base y un componente externo se encarga de traducirlas al SQL concreto, cambiar de base deja de ser un problema. Eso es una capa de abstracción, y su costo es el de siempre: obliga a usar una sintaxis propia —hay que aprenderla—, y a cambio hace el trabajo de adaptar y optimizar el SQL para el gestor que se esté usando.
Cómo se ve un ORM en el código
Las tres operaciones de un ORM bien hecho, que son las que muestran para qué sirve:
// 1) Guardar un objeto: el ORM genera todo el SQL
orm.save(object);
// 2) Traer un objeto complejo desde la base a memoria (con sus relaciones)
myObject = orm.get(MyObject.class, objectId);
// 3) Hacer consultas sobre las tablas
List myObjects = orm.find("FROM MyObject object WHERE object.property = 5");
Si se comparan con lo de la lección anterior, esos tres métodos tienen su equivalente en los create y retrieve que se implementan en un DAO (Data Access Object, objeto de acceso a datos): un INSERT armado a mano, un SELECT ... WHERE id = ? y un SELECT ... WHERE. La diferencia es que estos son más sencillos e intuitivos y reducen las fallas, porque no hay que escribir todo el código SQL: lo genera el ORM.
Hibernate y los ORM
Hibernate es una herramienta open source de ORM para la plataforma Java, que facilita el mapeo de atributos entre una base de datos relacional tradicional y el modelo de objetos de una aplicación, mediante archivos declarativos (XML) o anotaciones.
Lo que busca resolver es exactamente la diferencia entre los dos modelos de datos que conviven en una aplicación: el que se usa en la memoria de la computadora —orientado a objetos— y el que se usa en la base de datos —relacional—.
Para eso, quien desarrolla describe su modelo de datos, qué relaciones existen y qué forma tienen. Con esa información, la aplicación puede manipular los datos de la base operando sobre objetos, con todas las características de la POO de las lecciones 1 y 3. Y Hibernate hace el resto: convierte los datos entre los tipos de Java y los de SQL, genera las sentencias SQL y libera del manejo manual de los resultados, manteniendo la portabilidad entre gestores con un ligero incremento en el tiempo de ejecución.
Las interfaces que se usan
Un gestor de persistencia transparente —que es lo que es un ORM— da una API con cuatro servicios:
- Las operaciones básicas CRUD (Create, Read, Update, Delete), que son las altas, bajas, modificaciones y consultas del ABMC de la lección anterior.
- La ejecución de consultas.
- El control de transacciones, con el mismo
commityrollbackde la lección anterior. - La gestión de la caché a nivel de transacciones, que es lo que evita ir a la base por un objeto que ya se trajo hace un momento.
En Hibernate se usan cuatro interfaces: Session, Query, Criteria y Transaction. La más importante es Session, que es la que se asocia con el gestor de persistencia: es a través de ella que se guardan, se traen y se eliminan los objetos.
Las dependencias de Maven
Una forma rápida de obtener las bibliotecas de Hibernate es crear un proyecto Maven y agregar en el archivo pom.xml la dependencia del ORM y las demás que necesita.
Hacen falta tres cosas: jakarta.persistence (la API de persistencia), hibernate-core (el núcleo) y el conector de la base que se vaya a usar (el mismo de la lección 7).
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ar.com.ejemplo</groupId>
<artifactId>EjemploHibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.1.3.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.common</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>6.0.2.Final</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
Maven se ocupa de bajar esas bibliotecas y las que ellas necesitan, y de armar el proyecto. Es la misma idea de la dependencia del conector de la lección anterior, pero aplicada a todo el proyecto: en lugar de copiar archivos .jar en la carpeta de Java, se declaran una vez y Maven los resuelve.
El archivo de configuración
Para indicarle a Hibernate cuáles son los datos de la conexión —usuario, contraseña y demás parámetros— se edita un archivo XML llamado hibernate.cfg.xml, que hay que crear en src/main/resources.
XML viene de eXtensible Markup Language, «lenguaje de marcas extensible». Es un metalenguaje —un lenguaje que se usa para decir algo sobre otro— desarrollado por el W3C, la organización que elabora las recomendaciones para la web. Su campo de aplicación es enorme: bases de datos, documentos de texto, hojas de cálculo, páginas web. Aparece como el estándar que estructura el intercambio de información entre plataformas diferentes, que es exactamente lo que hace falta acá.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">org.mariadb.jdbc.Driver</property>
<property name="connection.url">jdbc:mariadb://localhost/database</property>
<property name="connection.username">user</property>
<property name="connection.password">pass</property>
<property name="connection.pool_size">3</property>
<property name="current_session_context_class">thread</property>
<property name="hibernate.show_sql">true</property>
<property name="format_sql">true</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- mapeo de las clases -->
<mapping resource="Persona.hbm.xml" />
<mapping class="Modelo.PersonaEntity" />
</session-factory>
</hibernate-configuration>
Qué significa cada propiedad
connection.driver_class— la clase que va a proveer el acceso a la base: la que envía las consultas SQL al motor y devuelve la respuesta. Es el mismo controlador de JDBC de la lección anterior.connection.url— la ruta de conexión: el protocolo, la IP del servidor, el nombre de la base.jdbc:mariadb://host:puerto/base, y se le pueden agregar el usuario y la contraseña como parámetros:...?user=usuario&password=clave.connection.usernameyconnection.password— la cuenta con la que se conecta. Ojo: en un proyecto de verdad estos datos no van escritos acá, se leen de variables de entorno o de un archivo que no se publica.show_sqlyformat_sql— recibentrueofalsey hacen que Hibernate muestre en tiempo de ejecución las consultas SQL que genera. Ponerlas entruemientras se aprende es lo más útil que se puede hacer: permite ver exactamente qué SQL está escribiendo el ORM por nosotros.
La propiedad hbm2ddl.auto
Esta merece su propio apartado porque puede o no afectar al esquema de la base de datos: según su valor, Hibernate puede crear tablas, modificarlas o borrarlas. Los cuatro valores:
| Valor | Qué hace |
|---|---|
none | No hace nada: el esquema no se genera. |
create | Borra las tablas si existen y las vuelve a crear tal como están definidas en los archivos .hbm.xml o en las anotaciones. Es lo que se usa en los casos de prueba: se crea el esquema, se cargan datos de prueba, se ejecutan las pruebas y al limpiar se lo elimina, dejando la base vacía. |
validate | Valida que el esquema de la base coincida con las clases, pero no hace ningún cambio. Si el esquema no existe, no lo crea: da un error del tipo Table not found. Es un control de consistencia. |
update | Actualiza el esquema comparando lo que hay en la base con lo que dicen las clases. Hibernate consulta los metadatos —lo de la lección 7— y trata de ajustar el esquema sobre la marcha: agrega columnas nuevas, restricciones. Lo que nunca hace es eliminar una columna o restricción que ya no forma parte del modelo. Si el esquema no existe, lo crea. |
none, o directamente no especificar la propiedad. La razón es que lo habitual es que un administrador de base de datos revise los scripts de migración antes de aplicarlos, sobre todo si la base la comparten varios sistemas. Dejar que un programa modifique el esquema solo es cómodo mientras se programa y peligroso cuando hay datos reales adentro.
Obtener la SessionFactory
Una vez creado el archivo de configuración hace falta obtener una instancia de SessionFactory, que es la que provee acceso a la sesión actual de Hibernate y la que procesa todos los pedidos de mapeo:
import org.hibernate.SessionFactory;
import org.hibernate.Session;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.HibernateException;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
private static Session session;
static {
try {
StandardServiceRegistry ssr =
new StandardServiceRegistryBuilder().configure("hibernate.cfg.xml").build();
Metadata meta = new MetadataSources(ssr).getMetadataBuilder().build();
sessionFactory = meta.getSessionFactoryBuilder().build();
session = sessionFactory.openSession();
} catch (HibernateException ex) {
System.err.println("Error al crear la conf de hibernate: " + ex.getMessage());
throw new ExceptionInInitializerError();
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}
Fijate en el bloque static: se ejecuta una sola vez, cuando la clase se carga, y arma la fábrica de sesiones una única vez para todo el programa. Es la misma idea del Singleton de la lección 5 —un recurso que se gestiona de forma centralizada y un único punto de acceso—, resuelto acá con las herramientas del lenguaje en lugar de con el patrón completo.
El archivo de mapeo
Para indicarle a Hibernate cómo vincular las clases de entidad con las tablas se usan otros archivos XML llamados HBM (Hibernate Mapping File). La buena práctica es un archivo de mapeo por cada clase de entidad, y su estructura es esta:
<?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="NombreDeLaClase" table="tabla_en_base_de_datos">
<!-- PROPIEDADES -->
</class>
</hibernate-mapping>
Dentro de la sección class hay que identificar todas las propiedades a mapear de la clase: tanto los atributos como las relaciones y las colecciones.
Y hay un paso que se olvida siempre y hace que el mapeo no se aplique: el archivo tiene que estar en la misma carpeta que el archivo de configuración, y hay que agregar su referencia en hibernate.cfg.xml:
<mapping resource="Persona.hbm.xml"/>
La clase que se va a mapear
public class Persona {
private Integer idpersona;
private String nombre;
private String apellido;
private String dni;
private Integer edad;
public Persona() {
}
public Persona(String nombre, String apellido, String dni, Integer edad) {
this.nombre = nombre;
this.apellido = apellido;
this.dni = dni;
this.edad = edad;
}
public Integer getIdpersona() {
return idpersona;
}
private void setIdpersona(Integer idpersona) {
this.idpersona = idpersona;
}
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
// ... el resto de los get y set
}
Mirá tres detalles del diseño de esa clase, que están puestos a propósito para que Hibernate funcione: tiene un constructor sin argumentos, el setIdpersona es privado —porque la clave la genera la base, no el programa— y los atributos son objetos (Integer) y no primitivos (int), para poder distinguir el valor 0 de «todavía no tiene valor».
Los elementos del archivo de mapeo
Un archivo de mapeo completo tiene tres elementos que hay que entender, y cada uno tiene sus atributos:
-
<id>— es obligatorio y define la clave primaria de la tabla. Sus atributos:name(el nombre del atributo de la clase que es la clave),column(el nombre de la columna en la base),type(un tipo soportado por Hibernate),length(el tamaño de la columna),access(si se accede por atributo o porget/set) yunsaved-value(el valor que tiene el atributo cuando el objeto se creó pero todavía no se guardó). -
<generator>— va adentro del<id>y le indica a Hibernate qué estrategia usar para asignar los identificadores nuevos. Los más usados:Nombre Qué hace incrementGenera claves que empiezan en 1 y se incrementan de a 1. Se puede aplicar a int,shortylong. Es portable.identityUsa el tipo identity que provee el propio gestor de base de datos. sequenceUsa las secuencias, que también provee el gestor. nativeElige automáticamente entre sequence,identityyhilosegún el gestor. Es el que conviene cuando se quiere portabilidad.hiloUsa un algoritmo hi/lo para generar identificadores únicos, con una tabla y una columna de la base del tipo long,shortoint.guidGenera un identificador globalmente único. No es portable. uuidLa clave se compone de la dirección IP local, la hora de la JVM, la hora del sistema y un valor continuo. No garantiza que sea única. -
<property>— declara una propiedad persistente de la clase, que se corresponde con una columna. Sus atributos principales:name(el nombre del atributo, obligatorio y empezando con minúscula),column(el nombre de la columna, que por defecto es el del atributo),type,length,not-null,unique,insertyupdate(para decir si la columna participa delINSERTo delUPDATE),precisionyscalepara los numéricos, yformula, que permite declarar una propiedad calculada con unSELECT.
Un ejemplo de property que muestra para qué sirve el atributo column: cuando el atributo de la clase y la columna de la tabla no se llaman igual, hay que decirlo. Y el type se indica cuando Hibernate no puede deducirlo solo, como pasa con los números guardados en java.math.BigDecimal:
<property name="precio" column="precio_en_pesos" type="float" />
Los tipos de datos de Hibernate
Hibernate tiene sus propios nombres de tipo, y cada uno se corresponde con uno o más tipos de Java. Es la tabla que hace falta tener a mano cuando se escribe un type en un archivo de mapeo o en una anotación:
| Tipo de Hibernate | Equivalencia en Java |
|---|---|
int | int, java.lang.Integer |
short | short, java.lang.Short |
long | long, java.lang.Long |
float | float, java.lang.Float |
double | double, java.lang.Double |
character | char, java.lang.Character |
byte | byte, java.lang.Byte |
boolean | boolean, java.lang.Boolean |
string | java.lang.String |
text | java.lang.String (campos largos) |
date, time | java.util.Date |
calendar | java.util.Calendar |
big_decimal | java.math.BigDecimal |
big_integer | java.math.BigInteger |
locale | java.util.Locale |
timezone | java.util.TimeZone |
currency | java.util.Currency |
class | java.lang.Class |
binary | byte[] |
serializable | java.io.Serializable |
clob | java.sql.Clob (texto muy grande) |
blob | java.sql.Blob (datos binarios muy grandes) |
La tabla se lee en los dos sentidos: sirve para saber qué escribir en un mapeo, y también para entender qué tipo de Java le va a dar Hibernate a un atributo declarado como clob. Y las dos últimas filas son las que aparecen cuando hay que guardar una imagen o un documento: blob para datos binarios, clob para texto largo.
Clases con anotaciones JPA
Desde la versión 3, Hibernate permite usar anotaciones de Java para especificar la relación entre las clases y las tablas. Gracias a eso ya no hacen falta los archivos .hbm.xml: hay que mantener menos archivos —solo el .java— y se escribe menos, porque la mayoría de las opciones tienen valores por defecto.
Esa forma de trabajar, usando los valores por defecto, es muy recomendable: en la mayoría de los casos alcanza con decir que la clase es una entidad y cuál es el campo identificador, y poco más.
Y hay una ventaja más, que es la razón por la que se usa: las anotaciones de Hibernate son compatibles con las de JPA (Jakarta Persistence API, la capa de persistencia de EJB 3). O sea que el mismo código sirve con cualquier implementación de JPA, no solo con Hibernate. Las anotaciones estándar están en el paquete jakarta.persistence, que hay que importar.
| Anotación | Qué marca |
|---|---|
@Entity | Marca la clase como una entidad persistente. La clase debe tener un constructor sin argumentos, visible al menos con alcance protegido. |
@Table | Permite especificar los detalles de la tabla que se va a usar para guardar la entidad: sobre todo, el nombre, para cuando no coincide con el de la clase. |
@Id | Marca el atributo que es la clave primaria. |
@GeneratedValue | Indica que el valor de la clave lo genera la base, con una estrategia determinada. Es el equivalente del elemento <generator>. |
@Column | Detalla la columna correspondiente: nombre, si admite nulos, longitud. |
@Embeddable | Indica que una clase que no es una entidad se puede incrustar dentro de otra entidad como si fuera una propiedad. Como no es una entidad, esa clase no se mapea a una tabla propia: sus atributos pasan a ser parte de la tabla de la clase que la contiene. Es exactamente la composición de la lección 3. |
El resultado en el código es una clase que se lee de un vistazo y dice todo lo que Hibernate necesita, sin ningún archivo aparte:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@Entity
@Table(name = "personas")
public class PersonaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer idpersona;
private String nombre;
private String apellido;
private String dni;
private Integer edad;
public PersonaEntity() {
}
// ... get y set
}
Las relaciones entre tablas
Las relaciones entre las clases tienen que estar mapeadas, y son las mismas tres de la lección 3. Cada una tiene su anotación:
| Anotación | Qué representa | Ejemplo |
|---|---|---|
@OneToOne |
Uno a uno: un objeto se relaciona con exactamente otro. En las tablas se resuelve con una clave foránea. | Una Persona y su Pasaporte. |
@OneToMany |
Uno a muchos: un objeto tiene una colección de otros. Es la relación más frecuente, y la que se ve en la sección siguiente. | Un Curso y sus Alumnos; un Instituto y sus Aulas. |
@ManyToMany |
Muchos a muchos: cada objeto se relaciona con varios del otro lado. En las tablas hace falta una tabla intermedia. | Los Alumnos y las Materias: un alumno cursa varias materias y una materia tiene varios alumnos. |
Los elementos del archivo XML equivalentes —para el mapeo sin anotaciones— tienen sus propios atributos, y son los mismos nombres que se acaban de ver. Los que conviene conocer son los que aparecen siempre:
- En uno a uno:
namepara el atributo,classpara la clase del otro lado,cascadepara indicar si las operaciones se propagan —si al guardar una persona se guarda también su pasaporte—, yconstrained. - En uno a muchos:
name,class,column(la columna que funciona como clave foránea),cascade,lazy(si la colección se trae de la base solo cuando se la usa) einverse, que define de qué lado se administra la relación. - En muchos a muchos:
name,class,table(la tabla intermedia), y adentro las columnas que apuntan a cada lado.
El cascade y el lazy son los dos que más se usan y los que más sorpresas dan. cascade decide si al guardar un objeto se guardan sus relacionados, o si hay que guardarlos uno por uno. Y lazy="true" —que es el valor por defecto— hace que una colección no se traiga de la base hasta que se la usa: es lo que evita que cargar un instituto traiga de golpe sus cinco mil aulas. Pero si después se intenta recorrer esa colección cuando la sesión ya se cerró, viene el error clásico de Hibernate, ese que dice LazyInitializationException.
Los elementos de colección
Cuando un objeto tiene una colección de otros —la relación uno a muchos—, hay que decirle a Hibernate con qué estructura se va a representar esa colección, y la elección no es indiferente: cada una ordena los elementos de una manera distinta. Los elementos disponibles son cinco:
| Elemento | Qué garantiza |
|---|---|
<set> | Que no haya elementos repetidos. En una relación uno a muchos se usa cuando interesa el conjunto y no el orden. Es el más común. |
<list> | Que el orden de los elementos sea el que se guardó. Necesita una columna de índice en la tabla para poder mantener la posición. |
<bag> | Es como una lista pero sin columna de índice: admite repetidos y no garantiza ningún orden. Es el más parecido a un ArrayList suelto. |
<idbag> | Como un bag pero con un identificador propio. Permite manejar la colección aunque los elementos se repitan exactamente iguales, y es la forma de trabajar con una tabla intermedia de muchos a muchos que tenga datos propios. |
<map> | Que a cada elemento se acceda por una clave, como en el Map de la lección 4. |
La decisión es la misma que se toma al elegir una colección de Java, y por las mismas razones: la estructura que mejor describa lo que el problema necesita. Si lo que importa es que no haya duplicados, set; si importa el orden, list; si hay que buscar por una clave, map.
Las operaciones sobre entidades
Todo el trabajo con Hibernate se hace a través de una Session, y las operaciones son las mismas que se escribieron a mano en la lección anterior. La diferencia es que acá no hay SQL: se trabaja con objetos.
| Operación | Cómo se hace | El SQL que generaba la lección 7 |
|---|---|---|
| Persistir un objeto nuevo | session.save(persona) o session.persist(persona) |
INSERT INTO personas (nombre, apellido, ...) VALUES (?, ?, ...) |
| Obtener una entidad por su clave primaria | session.get(Persona.class, id) |
SELECT * FROM personas WHERE id = ? |
| Modificar y actualizar | session.update(persona), o directamente modificar el objeto y hacer commit |
UPDATE personas SET nombre = ? WHERE id = ? |
| Eliminar | session.delete(persona) |
DELETE FROM personas WHERE id = ? |
| Buscar con otros criterios | Criteria (programático) o HQL (un lenguaje de consultas) |
SELECT * FROM personas WHERE edad > 21 |
Hay un detalle que sorprende a todos y que vale la pena remarcar: en Hibernate, si se guarda un objeto y después se lo modifica sin llamar a nada, el cambio se aplica solo al hacer commit. Eso es el dirty checking: la sesión compara los objetos que tiene en memoria con el estado con el que los trajo, y actualiza los que cambiaron. Es lo que permite dejar de escribir UPDATE a mano, y también es la razón de que convenga cerrar las sesiones.
El manejo de transacciones es el mismo de la lección 7, pero con la interfaz Transaction de Hibernate, que envuelve al commit y al rollback de JDBC:
Session session = sessionFactory.openSession();
Transaction tx = null;
try {
tx = session.beginTransaction();
session.save(persona);
session.save(otraPersona);
tx.commit(); // las dos operaciones se confirman juntas
} catch (Exception e) {
if (tx != null) {
tx.rollback(); // si algo fallo, no se hizo nada
}
e.printStackTrace();
} finally {
session.close();
}
Consultas: Criteria y HQL
Para las búsquedas, Hibernate ofrece dos caminos. Criteria API permite armar la consulta programáticamente, con objetos y métodos, lo que la hace verificable por el compilador y fácil de modificar según condiciones. Y HQL (Hibernate Query Language) es un lenguaje de consultas propio, parecido a SQL pero donde en lugar de tablas y columnas se escriben clases y atributos.
HQL
La consulta más simple trae todas las entidades de una clase:
FROM Persona
Que se ejecuta así, con la sesión y el resultado tipado:
Session session = sessionFactory.openSession();
try {
List<Persona> listado = session.createQuery("FROM Persona", Persona.class)
.getResultList();
for (Persona per : listado) {
System.out.println(per);
}
} catch (Exception e) {
System.out.println("Algo salio mal al listar las personas");
session.getSessionFactory().close();
}
session.close();
La cláusula WHERE
Funciona como en SQL: selecciona los resultados que coinciden con la condición. Las expresiones que admite son las esperables —operadores lógicos OR, AND, NOT; de igualdad =, <>, !=; de comparación <, >, <=, >=, like, between; matemáticos + - * /; concatenación ||; y para colecciones some, exists, all, any—.
Y los parámetros no se concatenan: se enlazan, que es lo que evita la inyección de la lección anterior. Se pueden enlazar por nombre o por posición:
// por nombre, con dos puntos adelante del nombre del parámetro
List<Persona> listado = session.createQuery(
"FROM Persona WHERE edad = :edadB", Persona.class)
.setParameter("edadB", 41)
.getResultList();
// por posición: el primer parámetro es el 0
Query q = session.createQuery("FROM Persona WHERE nombre = ? ");
q.setParameter(0, "Lucía");
Cuando la consulta devuelve solo algunas columnas en lugar de la entidad completa, el resultado es un arreglo de objetos:
List<Object[]> listado = session.createQuery(
"SELECT apellido, nombre, edad FROM Persona WHERE edad >= 21", Object[].class)
.getResultList();
for (Object[] valores : listado) {
System.out.println(valores[0] + " " + valores[1] + " - " + valores[2]);
}
Otros ejemplos del apunte, que muestran que la sintaxis es la de SQL con nombres de clases:
"SELECT apellido, nombre FROM Persona2 WHERE nombre LIKE 'mati%'"
"SELECT Persona.apellido, Grupo.nombre FROM Persona2, Grupo WHERE Persona.id = Grupo.idPersona"
"FROM Persona2 WHERE edad < 21 AND apellido LIKE 'G%'"
ORDER BY y GROUP BY
// ordenado por nombre
List<Persona> listado = session.createQuery(
"FROM Persona ORDER BY nombre asc", Persona.class).getResultList();
// agrupado por edad
List<Persona> listado = session.createQuery(
"FROM Persona GROUP BY edad", Persona.class).getResultList();
Paginación
La interfaz Query soporta paginar resultados, que es lo que hace falta para mostrar una lista de a diez sin traer diez mil filas:
Query query = session.createQuery("FROM Persona p ORDER BY nombre asc");
query.setMaxResults(10);
Los métodos de Query
| Método | Qué devuelve |
|---|---|
list() | El resultado de la consulta en una List. |
iterate() | El resultado en un Iterator. |
uniqueResult() | Un objeto, o nulo si la consulta no devuelve resultados. Es el que se usa cuando se busca por clave. |
executeUpdate() | Ejecuta una sentencia de modificación o borrado y devuelve la cantidad de entidades afectadas. |
getQueryString() | La consulta, como String. |
setFetchSize(size) | Fija cuántos resultados se recuperan en cada acceso a la base. |
setString, setInteger, setDouble, setDate, setCharacter | Asignan un valor a un parámetro de ese tipo. Tienen una versión por posición —empezando en 0— y otra por nombre. |
setParameter(posición|nombre, valor) | Asigna cualquier valor a un parámetro. Es la versión general de los anteriores. |
setParameterList(nombre, colección) | Asigna una colección de valores al parámetro indicado. |
Ejercicios
1. El problema que resuelve el ORM
Explicar, con un ejemplo propio, por qué el modelo de objetos y el relacional no son compatibles. Después indicar qué tres cosas hace Hibernate por el programador que con JDBC había que hacer a mano.
2. Leer un mapeo
Explicar qué hace cada línea de este archivo de mapeo, y decir a qué tabla y a qué columnas corresponde:
<hibernate-mapping>
<class name="Persona" table="personas">
<id name="idpersona" column="id" type="int">
<generator class="native" />
</id>
<property name="nombre" type="string" />
<property name="precio" column="precio_en_pesos" type="float" />
<property name="dni" type="string" unique="true" not-null="true" />
</class>
</hibernate-mapping>
3. hbm2ddl.auto
Indicar qué valor de hbm2ddl.auto corresponde en cada situación y por qué: (a) ejecutar la batería de pruebas automatizadas todas las noches; (b) el servidor de producción que comparte la base con otra aplicación; (c) la primera vez que se corre el proyecto en una computadora nueva.
4. Anotaciones o XML
Escribir la clase Alumno con las anotaciones JPA, sabiendo que se guarda en la tabla alumnos, que la clave legajo la genera la base y que los campos son nombre, apellido y promedio.
5. Elegir la colección
Decidir qué elemento de colección conviene en cada caso: (a) los alumnos de un curso, donde no puede haber dos inscripciones del mismo alumno; (b) los ítems de una factura, donde el orden importa y dos ítems pueden ser idénticos; (c) las materias de un alumno, donde se quiere buscar por código de materia.
6. Pasar de JDBC a HQL
Escribir en HQL lo que en JDBC sería cada una de estas consultas:
SELECT * FROM personas WHERE edad > 21 ORDER BY apellido
SELECT nombre, apellido FROM personas WHERE dni = '30123456'
UPDATE personas SET edad = 42 WHERE id = 7
Respuestas sugeridas
1. Un objeto tiene identidad propia, atributos con tipos de Java, relaciones que son referencias a otros objetos y, sobre todo, puede tener colecciones adentro. Una tabla solo tiene filas y columnas con tipos de SQL, las relaciones se resuelven con claves foráneas y no puede tener una lista en una columna. Por ejemplo, un objeto Factura con una List<Item> adentro no tiene una tabla que le corresponda directamente: hacen falta dos tablas y una relación. Lo que Hibernate hace por el programador: genera el SQL —los INSERT, SELECT, UPDATE y DELETE—, traduce entre los tipos de Java y de SQL, y resuelve el manejo de los resultados, sin tener que recorrer un ResultSet ni convertir cada valor.
2. La clase Persona se guarda en la tabla personas. El elemento <id> define la clave primaria: el atributo idpersona de la clase corresponde a la columna id de la tabla, y su valor lo genera Hibernate con la estrategia native, que elige la mejor según el gestor. El <property name="nombre"> mapea el atributo nombre con la columna del mismo nombre. El <property name="precio" column="precio_en_pesos"> mapea un atributo que en la base se llama distinto: eso es exactamente para lo que sirve el atributo column. Y el último tiene dos restricciones: unique="true" indica que no puede haber dos DNI iguales, y not-null="true" que el campo no admite nulos.
3. (a) create: se borra y se vuelve a crear el esquema en cada corrida, así las pruebas arrancan siempre del mismo estado conocido y no arrastran datos de la corrida anterior. (b) none, o directamente no poner la propiedad: nunca hay que dejar que un programa modifique el esquema de una base compartida en producción, donde hay datos reales y otras aplicaciones. Los cambios al esquema los revisa y aplica un administrador. (c) update: compara el esquema con las clases y crea las tablas que faltan, sin borrar nada. Es lo cómodo durante el desarrollo.
4.
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@Entity
@Table(name = "alumnos")
public class Alumno {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer legajo;
private String nombre;
private String apellido;
private Double promedio;
public Alumno() {
}
// ... get y set
}
No hace falta anotar los tres atributos porque, con los valores por defecto, Hibernate deduce el nombre de la columna a partir del nombre del atributo. Solo se anota lo que se aparta de lo predecible: la tabla porque se llama distinto, la clave y la forma en que se genera.
5. (a) <set>: la garantía de que no haya repetidos es justo lo que se necesita, y evita tener que controlarlo en el código. (b) <idbag> o <list>: si el orden importa y los ítems pueden ser idénticos —dos cafés iguales en el mismo pedido—, hace falta poder distinguirlos, y para eso está el idbag. Un <set> los tomaría como el mismo ítem. (c) <map>: el código de materia es la clave, y con ella se accede directo a la materia.
6.
"FROM Persona WHERE edad > 21 ORDER BY apellido"
"SELECT nombre, apellido FROM Persona WHERE dni = '30123456'"
"UPDATE Persona SET edad = 42 WHERE id = 7"
La diferencia está en los nombres: donde SQL dice la tabla personas, HQL dice la clase Persona; y donde SQL dice la columna apellido, HQL podría decir el atributo apellido. Esa es toda la traducción que hace el ORM, y es la razón por la que HQL no depende del gestor: nunca nombra nada de la base.
Resumen
- La persistencia es guardar los objetos fuera de la memoria. El problema de fondo es que el modelo de objetos y el relacional no son compatibles, y resolverlo a mano es lo que se hizo con JDBC.
- Un ORM hace ese mapeo automáticamente. Su ventaja es la reutilización:
save,getyfinden lugar de escribir SQL. Y una capa de abstracción permite además cambiar de gestor sin reescribir las consultas. - Hibernate es un ORM open source de Java: mapea con XML o anotaciones, convierte los tipos, genera el SQL y libera del manejo de los resultados. Sus interfaces son
Session—la principal—,Query,CriteriayTransaction. - Se configura con el
pom.xmlde Maven —jakarta.persistence,hibernate-corey el conector— y conhibernate.cfg.xml, donde van los datos de la conexión y las<mapping>. De ahí sale laSessionFactory. hbm2ddl.autodecide qué pasa con el esquema:noneno hace nada,createborra y crea —para pruebas—,validatesolo controla yupdateactualiza sin borrar. En producción,none.- El archivo de mapeo tiene tres elementos:
<id>—obligatorio, la clave primaria—,<generator>—la estrategia para generarla, connativecomo opción portable— y<property>—cada atributo persistente, concolumnpara cuando el nombre no coincide—. - Las anotaciones JPA reemplazan a los archivos XML:
@Entity,@Table,@Id,@GeneratedValue,@Columny@Embeddable. Son compatibles con cualquier implementación de JPA, no solo Hibernate. - Las relaciones se mapean con
@OneToOne,@OneToManyy@ManyToMany. Los atributoscascadeylazyson los que más sorpresas dan. - Las colecciones se representan como
<set>—sin repetidos—,<list>—ordenada—,<bag>,<idbag>y<map>—por clave—. - Las operaciones sobre entidades son persistir, obtener por clave, modificar y eliminar, más las transacciones con la interfaz
Transaction. El dirty checking hace que un objeto modificado se actualice solo al confirmar. - Las consultas se hacen con Criteria —programáticamente— o con HQL, que es SQL escrito en términos de clases y atributos, con parámetros enlazados por nombre o por posición, cláusulas
WHERE,ORDER BYyGROUP BY, y paginación consetMaxResults.
Con esto el programa guarda objetos sin escribir SQL. Lo que falta es lo último: publicarlo en la web, para que lo use cualquiera desde un navegador. Eso es la lección 9.
Licencia. Obra derivada de los apuntes de clase del Prof. Matías E. García —Clase Teórica 7—, publicada en profmatiasgarcia.com.ar bajo CC BY-NC-SA 2.5 Argentina; el desarrollo del texto, los diagramas y los ejercicios son de Prof Mauro Bobyk.