bases-de-dades,

Hibernate i el mapatge objecte-relacional

Fèlix Fèlix Segueix 01/07/2013 · 21 minuts de lectura
Hibernate i el mapatge objecte-relacional
Compartir amb...

Cinquena part del curs Curs BBDD i Java on s’analitzarà com realitzar mapatge objecte-relacional entre objectes d’un llenguatge orientat a objectes d’alt nivell (Java) amb bases de dades relacionals tradicionals i les tècniques associades per a l’extracció i inserció de dades en aquests casos.

1. Introducció al mapatge objecte-relacional

Havíem vist en l’activitat anterior que utilitzar JDBC per accedir a les dades allotjades a una base de dades és una tècnica vàlida i funcional.

Malgrat tot, havíem vist que malauradament presenta un seguit d’inconvenients. Un d’ells és el fet que per associar objectes Java amb entrades de taules de base de dades, l’obtenció i actualització de les dades dels objectes i de la base de dades s’ha de realitzar manualment programant crides específiques a sentències SQL de tipus SELECT, INSERT, UPDATE o DELETE (les operacions que anomenàvem CRUD).

Això és més incòmode encara quan pensem en el procés d’importació i exportació de dades entre la base de dades i els objectes Java en memòria, on hem de programar bucles que realitzin fila a fila (i objecte a objecte) la importació o exportació de dades.

Una tècnica que redueix l’impacte en la longitud dels nostres programes i millora la redacció de codi és el mapatge objecte-relacional (Object-Relational Mapping, ORM). Aquesta tècnica permet associar objectes Java amb dades de les taules d’una base de dades externa. El mapatge ens permet indicar la correspondència a nivell de camps de taula amb els atributs equivalents d’objecte, de manera que s’automatitza bona part del procés de traducció, eliminant una part de la programació necessària que faríem amb JDBC.

2. Hibernate

El producte lliure Hibernate és un API compatible que podem utilitzar per efectuar tasques de mapatge ORM.

Utilitzarem una classe POJO (Plain Old Java Object) com a receptacle de les files que provenen de la base de dades, i indicarem a Hibernate que utilitzi aquesta classe per associar-la a la taula de la qual traurem la informació.

El cas concret de Hibernate ens ofereix diverses alternatives per tal de realitzar el mapatge, entre les que destaquen dues:

  • A través d’un arxiu de definició XML que indica quins atributs de la classe POJO corresponen a quines columnes de la taula.

  • A través d’anotacions sobre les classes POJO de dades, afegint indicacions que Java utilitzarà per obtenir les associacions d’atributs de classe a columnes de taula.

A més, en el cas concret de Hibernate, se’ns ofereixen tècniques especials per accedir a la informació, construir consultes SQL, o fins i tot un llenguatge propi de consultes anomenat HQL.

3. Enllaçat/instal·lació de Hibernate

Per a què una aplicació faci ús del API de Hibernate, haurem de disposar de les classes corresponents en un lloc accessible del sistema. Per tant, s’haurà de descarregar des d’Internet la versió més adient (normalment la més moderna).

Aleshores haurem de disposar dels arxius JAR de la distribució de Hibernate al mateix directori de les nostres classes, o bé en un lloc al què apuntarem mitjançant el CLASSPATH en compilar i en executar.

Hibernate ens ofereix multitud de funcionalitats, però nosaltres només utilitzarem les funcionalitats bàsiques. Per tant, hem d’investigar els arxius JAR que apareixen en el directori required del pack d’arxius descarregats.

Així doncs, necessitarem els arxius bàsics (requerits) de Hibernate, el connector a la base de dades i també haurem de descarregar la versió adient del API log4j (si volem ajustar paràmetres del logging).

En el moment d’escriure aquestes notes s’ha treballat amb Hibernate-4.1.8 i log4j-1.2.17.

Per tant, tenim els següents arxius:

antlr-2.7.7.jar                                
dom4j-1.6.1.jar                                
hibernate-commons-annotations-4.0.1.Final.jar  
hibernate-core-4.1.8.Final.jar                 
hibernate-jpa-2.0-api-1.0.1.Final.jar           
javassist-3.15.0-GA.jar                         
jboss-logging-3.1.0.GA.jar
jboss-transaction-api_1.1_spec-1.0.0.Final.jar
log4j-1.2.17.jar
mysql-connector-java.jar

Si disposem els arxius en un directori anomenat libs, aleshores podrem compilar manualment els programes Java fent:

javac -cp .:libs/* Classe.java

I executar el programa fent:

java -cp .:libs/* Classe

Observi que l’especificació del CLASSPATH amb el modificador -cp accepta més d’un camí a arxius o directoris, i en el cas d’arxius JAR és molt més còmode utilitzar el caràcter * per indicar tots els arxius JAR del directori en qüestió.

4. Configuració de Hibernate

Quan una aplicació requereix de l’ús de la funcionalitat que ofereix Hibernate, s’ha de configurar l’accés a dades que se li demanarà. Això normalment es fa utilitzant un arxiu de configuració escrit en XML i amb nom per defecte hibernate.cfg.xml. Aquest arxiu conté les directives principals que permeten a Hibernate localitzar i connectar amb el servidor de base de dades i configurar també com s’accedeixen als objectes.

En un cas general, aquest arxiu prendrà la forma següent:

Arxiu hibernate.cfg.xml de mostra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version='1.0' encoding='utf-8'?>
<!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>
    <property name="connection.url">jdbc:mysql://localhost/hr</property>
    <property name="connection.username">hr_user</property>
    <property name="connection.password">hr_pass</property>
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="show_sql">false</property>
    <property name="format_sql">false</property>
    <property name="use_sql_comments">false</property>
    <property name="generate_statistics">false</property>
    <property name="hbm2ddl.auto">validate</property>
    <property name="connection.pool_size">1</property>
    <property name="current_session_context_class">thread</property>
 
    <!-- Mapping files will go here.... -->
    ...

  </session-factory>
</hibernate-configuration>

S’ha indicat a les quatre primeres propietats el lloc on es configura el servidor, el nom de la base de dades, el nom de l’usuari i la contrasenya. Observarà que la sintaxi és la mateixa que en les cadenes JDBC de connexió a base de dades.

En el punt que s’ha indicat amb punts suspensius farem esment de les classes i altres elements que haurà de tenir present Hibernate per a l’aplicació concreta que estiguem escrivint. Els exemples mostrats posteriorment indicaran exemples d’ús i com s’omple en cada cas.

A més, Hibernate generarà un gran volum d’informació de registre i depuració a través de la consola. Si vol desactivar-la haurà de configurar el paquet log4j per a què redueixi (o elimini) la sortida de depuració pel terminal.

Una de les múltiples formes de fer-ho és a través d’una línia de codi com aquesta:

org.apache.log4j.Logger.getLogger("org.Hibernate")
                   .setLevel(org.apache.log4j.Level.FATAL);

Com veu s’ha fixat el nivell de logging a FATAL, que correspon a errors fatals que normalment avortaran l’execució de l’aplicació. Els possibles cassos que presenta aquesta enumeració són: ALL, DEBUG, ERROR, FATAL, INFO, OFF o WARN. Cadascun d’ells fixa un nivell de generació de logging diferent.

Una altra tècnica, que permet modificar el comportament sense haver d’escriure codi específic, es mitjançant l’arxiu de propietats log4j.properties. Aquest arxiu permet configurar el paquet log4j en diversos paràmetres.

Un exemple de configuració de log4j mitjançant arxiu de propietats podria ser el mostrat tot seguit:

Arxiu log4j.properties de mostra

1
2
3
4
5
6
7
#Log to Console as STDOUT
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

#Root Logger
log4j.rootLogger=FATAL, stdout

5. Exemples de Hibernate

En aquesta part parlarem dels 2 projectes que mostren la funcionalitat bàsica de Hibernate amb uns exemples d’ús senzills.

Els dos exemples mostren, cadascun, diferents formes d’establir els mapatges objecte-relacionals:

  • El primer (exemple número 6) mostra com realitzar el mapatge mitjançant arxiu XML
  • El segon (exemple número 7) mostra com fer-ho a través d’annotations.

Tots dos exemples utilitzen la mateixa base de dades hr.

L’arxiu hr_mysql.sql li permet recuperar la base de dades hr al seu estat original. Recordi que pot executar l’ordre source per executar aquell arxiu SQL manualment des del client de consola de MySQL

A més, tots dos exemples fixen un nivell de logging al cas FATAL, mitjançant un arxiu anomenat log4j.properties com el mostrat prèviament i que estarà disponible juntament amb els arxius de codi font de Java, dins del CLASSPATH de l’aplicació.

En tots dos casos es contemplaran quatre programes (ubicats en quatre classes amb els seus corresponents mètodes main():

  • El primer fa un llistat de tots els treballadors, ordenats pel cognom.
  • El segon mostra, per a cada treballador, en quin departament treballa i la informació del cap de dit departament.
  • El tercer pregunta a l’usuari el id d’un treballador i mostra informació relacionada (arbre jeràrquic de caps i informació del seu departament).
  • El quart pregunta per un id de treballador i li augmenta el sou en una quantitat sol·licitada també a l’usuari.

6. Exemple d’ús amb definicions XML

El primer exemple mostrat correspon a un cas d’ús de Hibernate amb mapatge ORM realitzat mitjançant arxiu XML de definició de mapatge.

Observi ara l’exemple número 6

Els arxius font que trobarà al directori són:

06_hibernate_config_xml/
  |-- src
  |     |-- com.flx.ex06
  |     |     |-- DAOHibernateTest1.java
  |     |     |-- DAOHibernateTest2.java
  |     |     |-- DAOHibernateTest3.java
  |     |     \-- DAOHibernateTest4.java
  |     |-- com.flx.ex06.model
  |     |     |-- Department.java
  |     |     |-- Employee.java
  |     |     \-- HrDAOHibernate.java
  |     \-- mappings
  |           |-- Department.hbm.xml
  |           \-- Employee.hbm.xml
  |-- hibernate.cfg.xml
  \-- log4j.properties

Passem ara a descriure’ls un a un, tot destacant els fets més importants:

  • Classes com.flx.ex06.model.Employee.java i com.flx.ex06.model.Department.java, que són els POJO bàsics per a les dades de les files de les taules. Aquestes classes són simples POJOs amb tots els atributs declarats, i els mètodes getter i setter associats.

  • Arxius mappings/Department.hbm.xml i mappings/Employee.hbm.xml que defineixen els mapatges concrets per a les taules i objectes associats en cada cas.

    Fixi’s que en aquests arxius s’indica quina classe està associada a quina taula de la base de dades, i posteriorment indiquem els noms dels camps que són utilitzats a la base de dades i al POJO associat. Comprovarà que les definicions de les claus primàries (els camps employeeId a la classe Employee, i departmentId a la classe Department) són una mica més complexes que simplement esmentar els seus noms. Pot descobrir que es marca com a generació automàtica d’identificadors, tal i com les taules d’una base de dades real.

  • Arxiu hibernate.cfg.xml que configura Hibernate i indica quins són els arxius de mapatge objecte-relacional que es volen efectuar.

    Aquest arxiu ja s’havia comentat anteriorment, ara ens hem de centrar en les darreres línies, on declarem que vagi a buscar els arxius XML de definició del mapatge per a les classes Employee i Department.

  • Arxiu log4j.properties per inhabilitar (configurar) la sortida de missatges (logging) per pantalla excepte en els casos d’error fatal.

  • Els programes de prova són com.flx.ex06.DAOHibernateTest1 fins a com.flx.ex06.DAOHibernateTest4.

  • Classe com.flx.ex06.model.HrDAOHibernate amb la definició del DAO que serveix per accedir al model de dades.

    Aquesta classe incorpora el mètode getSession() per establir la connexió a la base de dades i poder treballar amb ella. El procediment que conté s’ha de fer sempre així. A més, utilitza una tècnica mitjançant la qual no crea una sessió cada vegada que es crida, sinó que només establirà la connexió la primera vegada i després reutilitzarà la connexió establerta a partir d’aquell moment. Si l’aplicació no és molt exigent en rendiment i concurrència, ja és una bona tria.

    La classe, a més, conté els mètodes que es faran servir en els programes de mostra, amagant la implementació interna de la seva funcionalitat.

Els detalls sobre la manera d’escriure les consultes es deixa per al final del tema.

7. Exemple d’ús amb definicions per annotations

Una variant interessant és la declaració del mapatge per anotacions (annotations), on en comptes de definir les associacions per arxiu XML, simplement introduirem unes marques especialitzades que indiquin directives a Hibernate per detectar automàticament el mapatge sobre la classe POJO.

El segon grup d’exemples mostrat correspon a un cas d’ús de Hibernate amb mapatge ORM realitzat mitjançant annotations sobre les classes POJO.

Observi ara l’exemple número 7

Els arxius font que trobarà al directori són:

07_hibernate_config_annotations/
  |-- src
  |     |-- com.flx.ex07
  |     |     |-- DAOHibernateTest1.java
  |     |     |-- DAOHibernateTest2.java
  |     |     |-- DAOHibernateTest3.java
  |     |     \-- DAOHibernateTest4.java
  |     \-- com.flx.ex07.model
  |           |-- Department.java
  |           |-- Employee.java
  |           \-- HrDAOHibernate.java
  |-- hibernate.cfg.xml
  \-- log4j.properties

Observarà que ara no apareix un arxiu de definició de mapatge, perquè ja està inclòs en les classes Employee i Department. Observi com estan definides ara aquestes classes POJO.

Fixi’s que apareixen annotations que indiquen fets transcendents per al mapatge ORM.

En concret, l’anotació @Entity indica que aquesta classe és una entitat que s’ha d’associar a taula de base de dades, mentre que @Table(name="nom_de_taula") indica que el nom de la taula a associar és l’indicat com a argument. Així es pot indicar quina taula correspon a quina entitat.

Després només calen afegir les anotacions @Id i @GeneratedValue sobre el mètode getter de l’atribut que sigui clau primària de la taula. Això és així per permetre la generació automàtica de valors en aquest camp.

Com veu, les anotacions necessàries en una classe POJO senzilla són ben poques, i força lògiques.

En el cas concret dels atributs que són claus foranes, observi que s’han establert les unions de taula a través de codi com el següent:

@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="departmentId") 
public Department getDepartment() { return department; }

Aquestes estructures d’anotació permeten vincular objectes entre taules diferents. Indiquem el nom del camp (departmentId) que serà el de clau forana sobre l’atribut que s’obtindrà una vegada resolta la unió de taules (department).

L’especificació FetchType.LAZY indica a Hibernate que no cal que es resolgui la unió de taules i es busqui el registre referenciat a menys que sigui imprescindible (es consulti el seu valor). L’alternativa és indicar FetchType.EAGER, que intenta resoldre les relacions sempre que pugui.

Ara, només caldrà configurar Hibernate per tal que utilitzi les anotacions fetes sobre la classe model. Això ho fem des de l’arxiu hibernate.cfg.xml, on veurà que simplement esmentem les classes POJO que defineixen els nostres objectes de dades.

Amb tot, els programes de prova són exactament iguals, línia a línia. I és que les dues formes de treballar són intercanviables (i es podrien mesclar si es volgués). Tot i que és força habitual treballar sempre amb un mateix esquema dins de la mateixa aplicació.

El fet que no hi hagi canvis en el codi és un dels punts forts de Hibernate, que permet canviar (i fins i tot combinar) de model de mapatge ORM segons convingui al desenvolupador.

8. Persistència d’objectes a la base de dades

Hibernate, cada vegada que ha de portar una entitat de dades des de la base de dades al domini dels objectes Java, manté una referència a l’entitat original en les taules des d’on s’ha extret. Això, en notació Hibernate, es diu que l’objecte està managed.

Quan un objecte és managed si volem modificar-lo, simplement haurem de cridar al mètode Session.update(objecte), de la mateixa manera que si volem eliminar-lo de la base de dades, només caldrà cridar el mètode Session.delete(objecte). Hibernate farà tota la resta.

Una característica interessant de Hibernate és que ens permet evitar l’ús de sentències SQL en multitud de situacions:

  • Quan volem inserir a la taula un nou registre a partir d’un objecte existent, utilitzem el mètode Session.save(objecte) i Hibernate s’encarregarà de generar l’ordre INSERT adient.
  • Quan volem eliminar de la taula un registre existent que tenim associat en un objecte, utilitzarem el mètode Session.delete(objecte), i Hibernate generarà l’ordre DELETE corresponent.
  • Quan volem modificar un registre de la taula, utilitzarem el mètode Session.update(objecte), i Hibernate generarà l’ordre UPDATE adequada.

A més, Hibernate ens ofereix el control de transaccions bàsic, de manera que els canvis no es faran efectius definitivament en la base de dades mentre no tanquem la transacció. Les transaccions normalment es delimiten així:

Transaction tx = session.beginTransaction();
...
tx.commit();  // tx.rollback();

Això també es pot programar així:

session.beginTransaction();
...
session.getTransaction().commit();
// session.getTransaction().rollback();

Per tant, tota l’operativa està protegida pels mecanismes convencionals de transaccions.

Per altra banda, observi que no hem de fer cap procediment especial per controlar la replicació de dades entre memòria i base de dades. Quan tanquem una transacció amb la crida commit() les dades són portades des de memòria a la base de dades sense que sigui necessari codificar cap procediment especial.

9. Construccions de consultes SQL, HQL i Criteria

Quan volem realitzar una consulta de selecció (SELECT) és quan tenim moltes més opcions i possibilitats.

Les tres opcions més importants que ofereix Hibernate són:

  • Enviar sentències SQL construïdes manualment (native SQL query)
  • Utilitzar un objecte Criteria per construir la sentència SQL a través de mètodes específics
  • Utilitzar el llenguatge HQL propi de Hibernate per construir la sentència SQL

9.1. Consultes amb SQL

Per generar una consulta SQL amb Hibernate, només hem d’escriure la sentència SQL dins d’una crida a la funció Session.createSQLQuery(). Aquest mètode ens retorna un objecte SQLQuery, al qual podem demanar el resultat de la seva execució invocant SQLQuery.list():

SQLQuery q = session.createSQLQuery("SELECT * FROM employees");
List<Employee[]> result = q.list();

El valor retornat és una llista de files, on cada fila és un array indexat que ens permetrà accedir a les diferents columnes de resultat. Haurem d’utilitzar meta-dades per descobrir els tipus dels diferents elements en temps d’execució.

Això no és res més que una consulta com les que realitzàvem amb JDBC. Per associar i obtenir objectes a partir de la base de dades hem d’utilitzar una altra via: hem de cridar el mètode SQLQuery.addEntity() per indicar l’entitat que s’espera rebre d’aquesta consulta i així obtenir la llista d’objectes ja en la forma que ens interessa:

SQLQuery q = session.createSQLQuery("SELECT * FROM employees")
                .addEntity(Employee.class);
@SuppressWarnings("unchecked")
List<Employee> result = q.list();

Observi que s’ha fet una referència a la classe de l’entitat amb l’operador Employee.class.

Observarà també que s’ha afegit la línia @SuppressWarnings("unchecked") per demanar al compilador de Java que no generi un avís sobre la transformació de tipus que es produeix en l’obtenció de la llista de resultats de la consulta (aquesta directiva només afecta a la línia immediatament a continuació o el bloc iniciat en ella). Malauradament no hi ha una altra manera ràpida i senzilla per evitar l’aparició d’aquest missatge d’avís generat pel compilador i s’ha de recórrer a aquest mètode “brut” per evitar-lo: els avisos són normalment signes d’un mal disseny o d’una possible font de problemes a la llarga. En aquest cas malauradament no depèn de nosaltres sinó de la forma en què Hibernate està codificat amb la funció SQLQuery.list().

Sigui com sigui ara disposem de la llista d’objectes ubicats en memòria i podem utilitzar tota la potència de Java per manipular-los en memòria i realitzar les operacions necessàries de la nostra aplicació.

9.2. Consultes amb Criteria

Hibernate ens ofereix un mecanisme molt interessant per no fer servir consultes SQL en el llenguatge natiu de la base de dades. De fet, no té sentit fer consultes en SQL natiu en un entorn que ens permet no dependre del motor concret de base de dades utilitzat!

Així doncs, una primera solució és la que es mostra aquí. En ella, tota consulta es fonamenta en l’ús d’un seguit de mètodes que pengen de la classe Criteria i que ens permeten anar construint la consulta a passos.

Vegem un exemple:

Criteria c = session.createCriteria(Employee.class)
                    .add(Restrictions.or(
                        Restrictions.like("lastname", "K%"),
                        Restrictions.like("lastname", "M%")
                    )).
                    .add(Restrictions.isNotNull("commissionPct"))
                    .addOrder(Order.asc("lastname"));
@SuppressWarnings("unchecked")
List<Employee> emps = c.list();
...

Observi com es van afegint elements a la consulta Criteria mitjançant el mètode add() i que disposem d’una gran col·lecció de paràmetres de filtrat de registres basats en la classe Restrictions. Consulti la documentació oficial per a més informació.

9.3. Consultes amb HQL

Les consultes que utilizen el llenguatge HQL (Hibernate Query Language) tenen una dinàmica semblant a la de SQL estàndard, però modifica lleugerament algunes expressions en alguns casos concrets.

El procediment per construir una sentència HQL i executar-la pot ser tant senzill com el següent cas:

String hql = "SELECT e FROM employees e WHERE lastname LIKE 'K%'";
@SuppressWarnings("unchecked")
List<Employee> emps = session.createQuery(hql).list();

Fins i tot podem realitzar tasques d’assignació de paràmetres, com:

String hql = "SELECT e FROM employees e WHERE lastname= :cognom";
@SuppressWarnings("unchecked")
List<Employee> emps = session.createQuery(hql)
                        .setParameter("unnom", "King")
                        .list();

Amb HQL també podem realitzar INSERT, DELETE i UPDATE, amb també una sintaxi semblant a l’estàndard SQL:

String hql = 
    "UPDATE employees e SET e.firstname = :nomNou WHERE e.firstname = :nomAntic";
int updatedEntities = 
    session.createQuery( hql )
        .setString( "nomNou", "Steven" )
        .setString( "nomAntic", "Stefan" )
        .executeUpdate();

Consulti la documentació de JBoss/Hibernate per a més informació sobre HQL.


Fèlix
Written by Fèlix Segueix
Sóc el Fèlix! 42!