1 de mayo de 2009

HIbernate - Parte 1: Persistiendo Objetos Simples usando Mapeos en XML

En la mayoría de las aplicaciones empresariales, si no es que en todas, una parte muy importante es el almacenamiento de datos de forma que estos datos puedan sobrevivir más allá del tiempo que nuestra aplicación está encendida (ya sea en una aplicación standalone o en una aplicación web).

El almacén de datos más común son las bases de datos relacionales. La naturaleza de estas bases hace que no sea tan fácil usarlas en el almacenamiento de datos en aplicaciones orientadas a objetos (por las diferencias entre el modelo de datos de objetos y el modelo de datos relacionales), ya que para guardar un objeto debemos extraer cada una de sus propiedades que queremos persistir y armar con ellos una sentencia INSERT de SQL. De la misma forma, cuando queremos recuperar los datos de un objeto, debemos usar una sentencia SELECT de SQL y después extraer el valor de cada una de las columnas recuperadas y llenar así nuestro objeto.

Esto puede no parecer un problema en aplicaciones pequeñas, pero cuando comenzamos a tener muchos objetos que debemos guardar se vuelve algo muy pesado y engorroso, además de que consume mucho de nuestro tiempo que podríamos dedicar a mejorar la lógica de la aplicación, o a realizar pruebas de la misma.

Esto se hizo de esta manera durante muchos años, hasta que comenzaron a surgir las soluciones de Mapeo Objeto/Relacional (ORM por sus siglas en inglés). El mapeo objeto/relacional se refiere a una técnica de mapear representaciones de datos de un modelo de objetos a un modelo de datos relacionales con un esquema basado en SQL.

Hibernate, como la definen sus autores, es una herramienta de mapeo objeto/relacional para ambientes Java. Además no solo se encarga del mapeo de clases Java a tablas de la base de datos (y de regreso), sino que también maneja los queries y recuperación de datos, lo que puede reducir de forma significativa el tiempo de desarrollo que de otra forma gastaríamos manejando los datos de forma manual con SQL y JDBC, encargándose de esta forma de alrededor del 95% de las tareas comunes relacionadas con la persistencia de datos, manejando todos los problemas relativos con la base de datos particular con la que estemos trabajando, de forma transparente para nosotros como desarrolladores. Entonces, si cambiamos el manejador de base de datos no será necesario que modifiquemos todo el SQL que ya teníamos para adaptarse al SQL que maneja la nueva base de datos. Solo será necesario modificar una línea en un archivo de configuración de Hibernate, y este se encargará del resto.

En esta serie de tutoriales veremos cómo usar Hibernate para realizar las tareas más comunes cuando trabajamos con bases de datos. Existen dos formas de hacer los mapeos en Hibernate, la primera es a través de archivos de mapeo en XML, que es la forma que veremos en este primer tutorial. La otra forma es usando anotaciones, que dejaremos para la segunda. Así que comencemos.

Para los tutoriales usaremos MySQL 5.1 como base de datos. También deben bajar el conector para Java versión 5.1.7.

Creamos una base de datos llamada "pruebahibernate". No se preocupen por crear alguna tabla, ya que haremos que sea el propio Hibernate el que las genere. Ahora comencemos con nuestra aplicación.

Lo primero que haremos es crear una biblioteca de NetBeans con los jars básicos de Hibernate, de esta forma cada vez que queramos usar Hibernate en un proyecto solo tendremos que agregar esta biblioteca. En realidad no crearemos esta biblioteca desde cero, ya que NetBeans ya tiene una biblioteca de Hibernate 3.2.5, solo actualizaremos la biblioteca con la última versión de Hibernate, la 3.3.1 GA. Así que descargamos la última versión del core de Hibernate desde la página de descarga de Hibernate.

Una vez que descarguemos y descomprimamos el archivo correspondiente veremos que en la raíz hay dos jars de Hibernate: "hibernate3.jar" y "hibernate-testing.jar". "hibernate3.jar" contiene las clases principales para el uso de Hibernate. Existen varias clases de soporte que son usadas por Hibernate para su funcionamiento normal. Algunas son opcionales y otras son requeridas. Afortunadamente estas se encuentran separadas en el mismo archivo que hemos descargado. Las clases obligatorias, que son las que nos interesan en este caso, se encuentran en el directorio "lib\required".

Bien, entonces para actualizar la biblioteca abrimos el NetBeans y nos dirigimos al menú "Tools -> Libraries":



En la ventana que se abre buscamos la biblioteca "Hibernate" y la seleccionamos. Con esto se mostrarán los archivos que la conforman. Seleccionamos todos los archivos y presionamos el botón "Remove" para eliminarlos de la biblioteca.



Una vez eliminados los archivos presionamos el botón "Add JAR/Folder..." para agregar los nuevos archivos:



Agregamos a la biblioteca los siguientes archivos:

  • hibernate3.jar

y del directorio de jars requeridos:

  • antlr-2.7.6.jar
  • commons-collections-3.1.jar
  • dom4j-1.6.1.jar
  • javassist-3.4.GA.jar
  • jta-1.1.jar

O sea, casi todos los archivos requeridos. Estamos excluyendo el archivo "slf4j-api-1.5.2.jar" porque si lo incluimos solo, se lanza una excepción. Lo que ocurre es que slf4j es una fachada para el logging, o bitácora de la aplicación (similar a "commons-logging" que estuvimos usando el los tutoriales de JasperReports). Esto significa que podemos usar el framework de logging que queramos (en teoría). Sin embargo en la distribución de Hibernate no se incluye ningún jar con una clase que implemente dicha fachada, por lo que debemos bajar uno nosotros mismos.

Podemos hacer dos cosas, la primer es agregar el archivo "slf4j-api-1.5.2.jar" que viene con Hibernate a nuestra biblioteca y el zip con la versión correspontiente de slf4j. De él extraemos "slf4j-simple-1.5.2.jar" (o el archivo que se ajuste al framework de logging que estemos usando) y lo incluimos en la biblioteca.

Lo otro que podemos hacer es, aprovechando que ya estamos bajando jars, descargar la última versión de slf4j de aquí (actualmente es la 1.5.6) y aprovechar para agregar los archivos "slf4j-api-1.5.6.jar" y "slf4j-simple-1.5.6.jar" a nuestra biblioteca. Yo haré esto último.

Una vez seleccionados estos archivos la biblioteca de Hibernate debe verse así:



Presionamos el botón "OK" y nuestra biblioteca ya estará actualizada.

Ahora creamos un nuevo proyecto de NetBeans (menú "File -> New Project... -> Java -> Java Application"). Le damos un nombre y una ubicación al proyecto y nos aseguramos de que las opciones "Create Main Class" y "Set as Main Project" estén habilitadas. Presionamos el botón "Finish" y veremos aparecer en el editor nuestra clase "Main".

Agregamos la biblioteca de "Hibernate", que creamos hace unos momentos, a nuestro proyecto. Hacemos clic derecho en el nodo "Libraries" del proyecto. En el menú contextual que se abre seleccionamos la opción "Add Library...":



En la ventana que se abre seleccionamos la biblioteca "Hibernate":



Presionamos el botón "Add Library" para que la biblioteca se agregue a nuestro proyecto. Aprovechamos también para agregar el conector de MySQL. Debemos tener los siguientes archivos en nuestro proyecto:



El siguiente paso es crear una clase cuyas instancias serán almacenadas como fila de una tabla en base de datos. Este tipo de objetos son llamados "Entidades". Para este ejemplo crearemos una pequeña y básica agenda, así que las entidades serán objetos de la clase "Contacto".

Los atributos que tendrá esta serán un nombre, un correo, y un número telefónico. Además, por regla, necesitamos un atributo que funcione como identificador para cada una de las entidades.

En este caso he hecho que la clase implementa la interface "java.io.Serializable", esto no es obligatorio pero es una buena práctica.

La clase "Contacto" queda de la siguiente forma:

public class Contacto implements Serializable
{
    private long id;
    private String nombre;
    private String email;
    private String telefono;

    public Contacto()
    {
    }

    public Contacto(String nombre, String email, String telefono)
    {
        this.nombre = nombre;
        this.email = email;
        this.telefono = telefono;
    }

    public String getEmail()
    {
        return email;
    }

    public void setEmail(String email)
    {
        this.email = email;
    }

    public long getId()
    {
        return id;
    }

    private void setId(long id)
    {
        this.id = id;
    }

    public String getNombre()
    {
        return nombre;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

    public String getTelefono()
    {
        return telefono;
    }

    public void setTelefono(String telefono)
    {
        this.telefono = telefono;
    }
}


La clase "Contacto" usa la convención de nombres JavaBeans para los getters y setters y visibilidad privada para los atributos. Este también es un diseño recomendado pero no obligatorio. Hibernate puede acceder a los campos o atributos de las entidades directamente.

El constructor sin argumentos si es obligatorio ya que Hibernate creará instancias de esta clase usando reflexión cuando recupere las entidades de la base de datos. Este constructor puede ser privado (si es que no quieren permitir que alguien más lo utilice), pero usualmente el nivel de acceso más restrictivo que usaremos es el de paquete (el default), ya que esto hace más eficiente la creación de los objetos.

La propiedad "id" mantendrá, como dije antes, un valor único que identificará a cada una de las instancias de "Contacto". Todas las clases de entidades persistentes deben tener una propiedad que sirva como identificador si queremos usar el conjunto completo de funcionalidades que nos ofrece Hibernate (y que veremos a lo largo de esta serie de tutoriales). De todas formas, la mayoría de las aplicaciones necesitan poder distinguir objetos de la misma clase mediante algún tipo de identificador. Usualmente no manipulamos directamente este identificador (dejamos que sea la base de datos quien lo genere, cuando la entidad sea guardada, y Hibernate quien lo asigne al objeto), por lo tanto el setter del "id" es privado (fíjense cómo lo he puesto en la clase "Contacto").

Hibernate puede acceder a los campos private directamente, así que no debemos preocuparnos por el hecho de que el identificador no pudiera ser establecido.

El siguiente paso es crear el archivo de mapeo. Si recuerdan, anteriormente dije que Hibernate es una herramienta de mapeos Objeto/Relacional, o sea que mapea los atributos de los objetos con las columnas de una tabla de una base de datos relacional. También dije que hay dos formas de hacerlo: mediante un archivo de mapeo en XML y mediante anotaciones y que en esta ocasión veríamos solo como usar los archivos de mapeo.

Crearemos un nuevo paquete que contendrá los archivos de mapeo, no es obligatorio tener los archivos de mapeo en un paquete separado ya que más adelante indicaremos donde se encuentra cada uno de los archivos, pero nos ayudará a mantener un poco de orden en nuestro proyecto. Hacemos clic derecho sobre el nodo "Source Packages" del proyecto para mostrar un menú contextual. De ese menú seleccionamos la opción "New -> Java Package..":



Nombramos a este paquete como "mapeos" y presionamos el botón "Finish" para agregar el nuevo paquete.

Ahora creamos un nuevo archivo XML que contendrá el mapeo de la clase "Contacto" con la tabla "CONTACTOS" de la base de datos. Por convención la extensión de estos archivos es ".hbm.xml" y tiene el mismo nombre de la entidad que está mapeando (aunque eso tampoco es necesario ya que podemos mapear más de una clase o entidad dentro del mismo archivo).

Hacemos clic derecho sobre el paquete que acabamos de crear. En el menú contextual que se abre seleccionamos la opción "New -> XML Document". Si no se muestra está opción seleccionamos "Other..." y en la ventana que se abre seleccionamos "XML -> XML Document":



Le damos al archivo el nombre "Contacto.hbm" (el asistente se encargará de agregar de forma automática el ".xml" al final):



Presionamos el botón "Next >" y se nos preguntará qué tipo de documento queremos crear: solo un documento XML bien formado, un documento regido por un DTD, o un documento regido por un Schema XML. Aunque el documento de mapeo está regido por un DTD nosotros elegiremos crear un documento XML bien formado ("Well-formed Document") ya que es más fácil simplemente pegarlo en el archivo que se creará que indicar mediante el asistente dónde se encuentra este archivo. Presionamos el botón "Finish" para que se nos muestre en el editor el nuevo archivo XML, el cual debe verse más o menos así:



Modificaremos este archivo. Eliminamos todo el contenido del archivo y lo reemplazamos por este:

<?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>
    
</hibernate-mapping>


Teniendo esta declaración, la cual indica el DTD que se usa para validar el archivo, el IDE nos ayudará a autocompletar las etiquetas y a asegurarnos que el archivo sea válido.

El elemento "<hibernate-mapping>" es el nodo raíz del documento y, por lo tanto, el resto de los elementos irán entre estas dos etiquetas.

El elemento con el que indicamos qué clase es la que estamos mapeando es el elemento "<class>" este elemento tiene los atributos "name" y "table" que nos permiten indicar el nombre completo de la clase y la tabla con la que será mapeada, respectivamente:

<hibernate-mapping>
    <class name="hibernatenormal.Contacto" table="CONTACTOS">

    </class>
</hibernate-mapping>


Con esto indicamos que las entidades de la clase "Contacto" serán almacenadas en la tabla "CONTACTOS". Ahora debemos indicar cuál de los elementos de la clase entidad es el identificador. Este identificador será mapeado con la llave primaria de la tabla, además como nosotros no manejaremos el valor del identificador, le indicamos a Hibernate cómo queremos que este valor sea generado (la estrategia de generación). Para esto usamos el elemento "<id>", indicando el nombre del atributo de la clase entidad que representa el identificador (que en este caso también se llama "id" ^-^). Opcionalmente en este elemento (como en el resto de los elementos de mapeo de propiedades) podemos indicar con qué columna queremos que se mapee usando el atributo "column":

<hibernate-mapping>
    <class name="hibernatenormal.Contacto" table="CONTACTOS">
        <id name="id" column="ID">
            <generator class="identity" />
        </id>
    </class>
</hibernate-mapping>


El elemento "<generator>" es el que nos permite indicar la estrategia de generación del identificador usando su atributo "class". Existen varias estrategias que están explicadas en esta página, pero las que usaremos más frecuentemente son "identity" (con la que Hibernate se encarga de generar el query necesario para que el nuevo identificador sea igual a el último identificador + 1) y "native" (con el que se usa la estrategia por default del manejador que estemos utilizando (dialecto)).

Para terminar con el mapeo incluimos las declaraciones para el resto de las propiedades persistentes (las que queremos que sean almacenadas) de la clase entidad ("nombre", "email", y "telefono") usando el elemento "<property>" en el cual indicamos el nombre de la propiedad como aparece en la clase entidad y, opcionalmente, el tipo de la propiedad y la columna con la que será mapeada usando los atributos "type" y "column" respectivamente:

<hibernate-mapping>
    <class name="hibernatenormal.Contacto" table="CONTACTOS">
        <id name="id" column="ID">
            <generator class="identity" />
        </id>
        <property name="nombre" type="string" column="NOMBRE" />
        <property name="email" />
        <property name="telefono" />
    </class>
</hibernate-mapping>


El archivo "Contacto.hbm.xml" final debe verse así:



¿Qué ocurre si no indicamos el tipo de la propiedad? En ese caso Hibernate utiliza nuevamente reflexión en nuestra clase para determinar el tipo del atributo y así elegir el tipo más adecuado para la columna de la tabla (en caso de que dejemos que Hibernate genere las tablas).

¿Qué ocurre si no indicamos el nombre de la columna con la que mapea una propiedad? Esto depende de si Hibernate está generando las tablas de la base de datos o las hemos generado nosotros mismos. Si es Hibernate quien está generando las tablas no hay problema, simplemente dará a la columna correspondiente el mismos nombre de la propiedad. Por ejemplo, para la propiedad "email" creará una columna "email". Sin embargo, si nosotros hemos creado las tablas hay un riesgo potencial de un error. Hibernate buscará en las consultas una columna con el mismo nombre de la propiedad, pero si hemos usado un nombre distinto, por ejemplo en vez de "email" hemos llamado a la columna "E_MAIL" ocurrirá una excepción indicando que no se ha encontrado la columna "email", así que en ese caso debemos tener cuidado de indicar el nombre de las columnas como están en la base de datos.

Bien, después de esta breve explicación podemos proseguir.

Ya que hemos configurado el mapeo de nuestra clase entidad debemos configurar Hibernate. Hibernate está en la capa de nuestra aplicación que se conecta a la base de datos (la capa de persistencia), así que necesita información de la conexión. La conexión se hace a través de un pool de conexiones JDBC que también debemos configurar. La distribución de Hibernate contiene muchos pools de conexiones JDBC Open Source, como por ejemplo C3P0, pero en esta ocasión usaremos el pool de conexiones que Hibernate trae integrado.

La configuración de Hibernate puede hacerse en tres lugares:

  • Un archivo de propiedades llamado "hibernate.properties".
  • Un archivo XML llamado "hibernate.cfg.xml".
  • En código dentro de la misma aplicación.

En realidad los archivos pueden tener cualquier nombre, pero Hibernate buscará por default los archivos con los nombres que he mencionado, en una ubicación predeterminada (la raíz del classpath de la aplicación). Si queremos usar archivos con otros nombres deberemos especificarlo en el código.

La mayoría, por lo que he podido ver en Internet, preferimos usar el archivo XML ya que, entre otras cosas, los IDEs nos ayudan a su creación y configuración gracias a que está regido por un DTD. Así que crearemos este archivo en nuestro proyecto. Hacemos clic derecho en el nodo "Source Packages" del proyecto. En el menú contextual que se abre seleccionamos "New -> XML Document...":



Nombramos al archivo "hibernate.cfg", el IDE se encargará de colocarle la extensión ".xml". Ubicamos el archivo en el directorio "src":



Con esto lograremos que el archivo quede en la raíz del classpath de la aplicación, también conocido como el paquete default. Presionamos el botón "Next >" y en la pantalla siguiente indicamos que queremos crear un documento XML bien formado (como en el caso anterior). Presionamos el botón "Finish " para generar el archivo. Si nos fijamos en el archivo generado, este debe encontrarse en un paquete llamado "<default package>":



Borramos todo el contenido del archivo generado y colocamos las siguientes líneas:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
</hibernate-configuration>


El DTD nos ayudará para que el IDE autocomplete las etiquetas.

Como podemos ver, el elemento raíz del archivo de configuración es "<hibernate-configuration>" y, por lo tanto, todos los elementos los colocaremos entre estas dos etiquetas.

Lo primero que debemos hacer es configurar un "session-factory", que básicamente es lo que le dice a Hibernate cómo conectarse y manejar la conexión a la base de datos. Podemos tener más de un "session-factory" en el archivo de configuración (por si quisiéramos conectarnos a más de una base de datos), pero por lo regular solo ponemos uno:

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


Lo siguiente es configurar la conexión a nuestra base de datos. En este archivo se configuran los parámetros básicos y típicos para una conexión (la URL, nombre de usuario, contraseña, driver, etc.). Cada uno de estos parámetros se configura dentro de una etiqueta "<property>" (al igual que casi todos los elementos del archivo de configuración). Como dije antes, usaré una base de datos MySQL para este ejemplo, así que mi configuración queda de la siguiente forma:

<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/pruebahibernate</property>
<property name="connection.username">usuario</property>
<property name="connection.password">password</property>


Después, configuramos el pool de conexiones de Hibernate. En este caso como es un ejemplo muy simple, solo nos interesa tener una conexión en el pool, por lo que colocamos la propiedad "connection.pool_size" con un valor de "1":

<property name="connection.pool_size">1</property>


El siguiente paso es muy importante. Debemos indicar el "dialecto" que usará Hibernate para comunicarse con la base de datos. Este dialecto es la variante de SQL que usa la base de datos para ejecutar queries. Indicamos el dialecto con el fully qualified class name, o el nombre completo de la clase incluyendo el paquete. En el caso de MySQL 5 usamos "org.hibernate.dialect.MySQL5Dialect". En esta página pueden encontrar una lista más o menos completa de los dialectos soportados por Hibernate, pero siempre es mejor revisar la documentación de la versión que estén usando para estar seguros:

<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>


Otras dos propiedades importantes que podemos configurar son: "show_sql" que indica si queremos que las consultas SQL generadas sean mostradas en el stdout (normalmente la consola), y "hbm2ddl.auto", que indica si queremos que se genere automáticamente el esquema de la base de datos (las tablas). "show_sql" puede tomar valores de "true" o "false", yo lo colocaré en "true" (lo que puede ser bueno mientras estamos en etapas de desarrollo o pruebas, pero querrán cambiar su valor cuando su aplicación pase a producción). Por otro lado "hbm2ddl.auto" puede tomar los valores, segun la documentación oficial(falta "none"), de "validate", "update", "create", y "create-drop" (aunque no todos los valores funcionan para todas las bases de datos). Yo los colocaré de la siguiente forma:

<property name="show_sql">true</property> 
<property name="hbm2ddl.auto">create-drop</property>


Con el valor "create-drop" hacemos que cada vez que se ejecute la aplicación Hibernate elimine las tablas de la base de datos y las vuelva a crear. Para terminar con este archivo de configuración, debemos indicar dónde se encuentra cada uno de los archivos de mapeo que hemos creado, usando el elemento "<mapping>". En nuestro caso solo hemos creado un archivo de mapeo, pero debemos colocar un elemento "<mapping>" por cada uno de los archivos que hayamos creado:

<mapping resource="mapeos/Contacto.hbm.xml"/>


En el elemento "resource" debemos colocar la ubicación de los archivos de mapeo dentro de la estructura de paquetes de la aplicación. El archivo de configuración "hibernate.cfg.xml" final debe verse así:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC 
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN" 
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
  
    <!-- parametros para la conexion a la base de datos -->
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="connection.url">jdbc:mysql://localhost/pruebahibernate</property>
    <property name="connection.username">usuario</property>
    <property name="connection.password">password</property>
    
    <!-- Configuracion del pool interno -->
    <property name="connection.pool_size">1</property>
    
    <!-- Dialecto de la base de datos -->
    <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
    
    <!-- Otras propiedades importantes -->
    <property name="show_sql">true</property>
    <property name="hbm2ddl.auto">create-drop</property>
    
    <!-- Archivos de mapeo -->
    <mapping resource="mapeos/Contacto.hbm.xml"/>
  </session-factory>
</hibernate-configuration>




Esta es toda la configuración que debemos hacer. Ahora veremos el código necesario para guardar y recuperar objetos "Contacto" de la base de datos.

Lo primero que haremos es crear una clase ayudante o de utilidad llamada "HibernateUtil", que se hará cargo de inicializar y hacer el acceso al "org.hibernate.SessionFactory" (el objeto encargado de gestionar las sesiones de conexión a la base de datos que configuramos en el archivo "hibernate.cfg.xml") más conveniente.

Dentro de esta clase declaramos un atributo static de tipo "SessionFactory", así nos aseguraremos de que solo existe una instancia en la aplicación. Además lo declararemos como final para que la referencia no pueda ser cambiada después de que la hayamos asignado.

private static final SessionFactory sessionFactory;


Después usamos un bloque de inicialización estático para inicializar esta variable en el momento en el que la clase sea cargada en la JVM.

Para realizar esta inicialización lo primero que se necesita es una instancia de la clase "org.hibernate.cfg.Configuration" que permite a la aplicación especificar las propiedades y documentos de mapeo que se usarán (es aquí donde indicamos todo si no queremos usar un archivo XML o de propiedades). Si usamos el método "configure()" que no recibe parámetros entonces Hibernate busca el archivo "hibernate.cfg.xml" que creamos anteriormente. Una vez que tenemos este objeto, entonces podemos inicializar la instancia de "SessionFactory" con su método "buildSessionFactory()". Además como este proceso puede lanzar "org.hibernate.HibernateException" (que extiende de "RuntimeException") la cachamos y lanzamos como un "ExceptionInInitializarError" (que es lo único que puede lanzarse desde un bloque de inicialización). El bloque de inicialización queda de la siguiente forma:

static
{
    try 
    { 
        sessionFactory = new Configuration().configure().buildSessionFactory();
    } catch (HibernateException he) 
    { 
        System.err.println("Ocurrió un error en la inicialización de la SessionFactory: " + he); 
        throw new ExceptionInInitializerError(he); 
    } 
}


Finalmente creamos un método static llamado "getSessionFactory()" para recuperar la instancia de la "SessionFactory":

public static SessionFactory getSessionFactory()
{ 
    return sessionFactory;
} 


La clase "HibernateUtil" queda de la siguiente forma:

import org.hibernate.HibernateException;
import org.hibernate.SessionFactory; 
import org.hibernate.cfg.Configuration;

public class HibernateUtil
{  
    private static final SessionFactory sessionFactory;   

    static 
    { 
        try 
        { 
            sessionFactory = new Configuration().configure().buildSessionFactory(); 
        } catch (HibernateException he) 
        { 
           System.err.println("Ocurrió un error en la inicialización de la SessionFactory: " + he); 
           throw new ExceptionInInitializerError(he); 
        } 
    }  

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




Bien ha llegado el momento, ahora escribiremos una clase DAO (no seguiremos el patrón al pie de la letra, es solo para mostrar cómo trabajar con Hibernate) que nos permitirá realizar operaciones de base de datos.

Creamos una clase llamada "ContactosDAO" y agregamos dos atributos, uno llamado "sesion" de tipo "org.hibernate.Session", y otro llamado "tx" de tipo "org.hibernate.Transaction".

private Session sesion;
private Transaction tx;


Estos atributos nos servirán para mantener la referencia a la sesión a base de datos, y a la transacción actual, respectivamente. Ahora agregaremos dos métodos de utilidad. El primero nos ayudará a iniciar una sesión y una transacción en la base de datos. Llamaremos a este método "iniciaOperacion", y la implementación es la siguiente:

private void iniciaOperacion() throws HibernateException
{
    sesion = HibernateUtil.getSessionFactory().openSession();
    tx = sesion.beginTransaction();
}


En el método anterior obtenemos una referencia a "SessionFactory" usando nuestra clase de utilidad "HibernateUtil". Una vez que tenemos la "SessionFactory" creamos una conexión a la base de datos e iniciamos una nueva sesión con el método "openSession()". Una vez teniendo la sesión iniciamos una nueva transacción y obtenemos una referencia a ella con "beginTransaction()".

Ahora el segundo método de utilidad (llamado "manejaExcepcion") nos ayudará a manejar las cosas en caso de que ocurra una excepción. Si esto pasa queremos que la transacción que estamos ejecutando se deshaga y se relance la excepción (o podríamos lanzar una propia). Por lo que el método queda así:

private void manejaExcepcion(HibernateException he) throws HibernateException
{
    tx.rollback();
    throw new HibernateException("Ocurrió un error en la capa de acceso a datos", he);
}


Ahora crearemos los métodos que nos permitirán realizar las tareas de persistencia de una entidad "Contacto", conocidas en lenguaje de base de datos como CRUD: guardarla, actualizarla, eliminarla, buscar un entidad "Contacto" y obtener todas los contactos que existen en la base de datos, así que comencemos.

Afortunadamente Hibernate hace que esto sea fácil ya que proporciona métodos para cada una de estas tareas. Primero veamos como guardar un objeto "Contacto". Para esto Hibernate proporciona el método "save" en el objeto de tipo "org.hibernate.Session", que se encarga de generar el "INSERT" apropiado para la entidad que estamos tratando de guardar. El método "guardaContacto" queda de la siguiente forma:

public long guardaContacto(Contacto contacto)
{ 
    long id = 0;  

    try 
    { 
        iniciaOperacion(); 
        id = (Long)sesion.save(contacto); 
        tx.commit(); 
    }catch(HibernateException he) 
    { 
        manejaExcepcion(he);
        throw he; 
    }finally 
    { 
        sesion.close(); 
    }  
    return id; 
}


Regresamos el "id" generado al guardar el "Contacto" solo por si queremos usarlo más adelante en el proceso (como lo haremos nosotros), o si queremos mostrarle al usuario, por alguna razón, el identificador del "Contacto".

Ahora veremos cómo actualizar un "Contacto". Para eso usamos el método "update" del objeto "sesion" en nuestro método "actualizaContacto":

public void actualizaContacto(Contacto contacto) throws HibernateException 
{ 
    try 
    { 
        iniciaOperacion(); 
        sesion.update(contacto); 
        tx.commit(); 
    }catch (HibernateException he) 
    { 
        manejaExcepcion(he); 
        throw he; 
    }finally 
    { 
        sesion.close(); 
    } 
}


Como podemos ver, el método para actualizar es muy similar al método para guardar la entidad. Lo mismo ocurre con el método para eliminarla, "eliminaContacto":

public void eliminaContacto(Contacto contacto) throws HibernateException 
{ 
    try 
    { 
        iniciaOperacion(); 
        sesion.delete(contacto); 
        tx.commit(); 
    } catch (HibernateException he) 
    { 
        manejaExcepcion(he); 
        throw he; 
    }finally 
    { 
        sesion.close(); 
    } 
}


Ahora veremos unos métodos un poco más interesantes.

Cuando queremos buscar una entidad podemos usar varios criterios. La forma más fácil es buscar una entidad particular usando su "id". La clase "org.hibernate.Session" proporciona dos métodos para esto: "load" y "get". Los dos hacen prácticamente lo mismo: en base al identificador y tipo de la entidad recuperan la entidad indicada, con la diferencia de que "load" lanza una excepción en caso de que la entidad indicada no sea encontrada en la base de datos, mientras que "get" simplemente regresa "null". Pueden usar el que prefieran, en lo personal me gusta más "get", así que lo usaré para el método "obtenContacto":

public Contacto obtenContacto(long idContacto) throws HibernateException
{ 
    Contacto contacto = null;  

    try 
    { 
        iniciaOperacion(); 
        contacto = (Contacto) sesion.get(Contacto.class, idContacto); 
    } finally 
    { 
        sesion.close(); 
    }  
    return contacto; 
}


Finalmente veremos el método "obtenListaContactos" que recupera todos los Contactos que estén guardados en la base de datos. Como en este caso regresaremos una lista de elementos deberemos crear una consulta. Cuando tenemos que crear una consulta con JDBC lo hacemos en SQL, sin embargo con Hibernate tenemos varias opciones:

  • Usar una query en SQL nativo.
  • Crear una query en HQL (Hibernate Query Language).
  • Crear una query usando Criteria que es un API para crear queries de una forma más "orientada a objetos".

Critera es, a mi parecer, la forma más fácil de crear las queries. Sin embargo, también en mi opinión, HQL es más poderosa y tenemos más control sobre lo que está ocurriendo, así que será esta forma la que usaremos.

Creando la consulta en HQL Hibernate la transformará al SQL apropiado para la base de datos que estemos usando. La consulta en realidad es muy simple: indicamos que queremos obtener datos (generar un "SELECT"), en este caso la clausula "SELECT" no es necesaria (aunque si existe) porque vamos a regresar todos los datos del objeto (podemos indicar solo unos cuantos atributos, eso es llamado proyección, pero se maneja de una forma distinta). Lo que si debemos indicar es de cuál clase queremos recuperar las instancias, por lo que necesitamos la clausula "FROM" y el nombre de la clase (recuerden que en base al nombre de la clase Hibernate sabrá en cuál tabla están almacenadas las entidades). Espero que esta explicación se haya entendido, se que es un poco enredado, pero quedará más claro con el ejemplo:

public List<Contacto> obtenListaContactos() throws HibernateException 
{ 
    List<Contacto> listaContactos = null;  
    
    try 
    { 
        iniciaOperacion(); 
        listaContactos = sesion.createQuery("from Contacto").list(); 
    }finally 
    { 
        sesion.close(); 
    }  

    return listaContactos; 
}


Eso es todo, nuestra consulta para recuperar todos los Contactos que tenemos en la base de datos solo debemos usar la clausula "FROM Contacto". Si quieren una referencia más amplia de HQL pueden encontrarla en el tutorial sobre HQL.

Bien, eso es todo. La clase "ContactosDAO" queda de la siguiente forma (omitiendo los imports):

public class ContactosDAO
{  
    private Session sesion; 
    private Transaction tx;  

    public long guardaContacto(Contacto contacto) throws HibernateException 
    { 
        long id = 0;  

        try 
        { 
            iniciaOperacion(); 
            id = (Long) sesion.save(contacto); 
            tx.commit(); 
        } catch (HibernateException he) 
        { 
            manejaExcepcion(he); 
            throw he; 
        } finally 
        { 
            sesion.close(); 
        }  

        return id; 
    }  

    public void actualizaContacto(Contacto contacto) throws HibernateException 
    { 
        try 
        { 
            iniciaOperacion(); 
            sesion.update(contacto); 
            tx.commit(); 
        } catch (HibernateException he) 
        { 
            manejaExcepcion(he); 
            throw he; 
        } finally 
        { 
            sesion.close(); 
        } 
    }  

    public void eliminaContacto(Contacto contacto) throws HibernateException 
    { 
        try 
        { 
            iniciaOperacion(); 
            sesion.delete(contacto); 
            tx.commit(); 
        } catch (HibernateException he) 
        { 
            manejaExcepcion(he); 
            throw he; 
        } finally 
        { 
            sesion.close(); 
        } 
    }  

    public Contacto obtenContacto(long idContacto) throws HibernateException 
    { 
        Contacto contacto = null;  
        try 
        { 
            iniciaOperacion(); 
            contacto = (Contacto) sesion.get(Contacto.class, idContacto); 
        } finally 
        { 
            sesion.close(); 
        }  

        return contacto; 
    }  

    public List<Contacto> obtenListaContactos() throws HibernateException 
    { 
        List<Contacto> listaContactos = null;  

        try 
        { 
            iniciaOperacion(); 
            listaContactos = sesion.createQuery("from Contacto").list(); 
        } finally 
        { 
            sesion.close(); 
        }  

        return listaContactos; 
    }  

    private void iniciaOperacion() throws HibernateException 
    { 
        sesion = HibernateUtil.getSessionFactory().openSession(); 
        tx = sesion.beginTransaction(); 
    }  

    private void manejaExcepcion(HibernateException he) throws HibernateException 
    { 
        tx.rollback(); 
        throw new HibernateException("Ocurrió un error en la capa de acceso a datos", he); 
    } 
}


Ahora, y para terminar el ejemplo, haremos uso de "ContactosDAO" para crear y recuperar algunas instancias de "Contacto". Creo que el código es muy claro así que no lo explicaré ^-^!. La clase "Main" queda de la siguiente forma:

public class Main
{ 
    public static void main(String[] args) 
    { 
        ContactosDAO contactosDAO = new ContactosDAO(); 
        Contacto contactoRecuperado = null;  
        long idAEliminar = 0;  

        //Creamos tes instancias de Contacto 
        Contacto contacto1 = new Contacto("Contacto 1", "contacto1@contacto.com", "12345678"); 
        Contacto contacto2 = new Contacto("Contacto 2", "contacto2@contacto.com", "87654321"); 
        Contacto contacto3 = new Contacto("Contacto 3", "contacto3@contacto.com", "45612378");  

        //Guardamos las tres instancias, guardamos el id del contacto1 para usarlo posteriormente 
        idAEliminar = contactosDAO.guardaContacto(contacto1); 
        contactosDAO.guardaContacto(contacto2); 
        contactosDAO.guardaContacto(contacto3);  

        //Modificamos el contacto 2 y lo actualizamos 
        contacto2.setNombre("Nuevo Contacto 2"); 
        contactosDAO.actualizaContacto(contacto2);  

        //Recuperamos el contacto1 de la base de datos 
        contactoRecuperado = contactosDAO.obtenContacto(idAEliminar); 
        System.out.println("Recuperamos a " + contactoRecuperado.getNombre());  

        //Eliminamos al contactoRecuperado (que es el contacto3) 
        contactosDAO.eliminaContacto(contactoRecuperado);  

        //Obtenemos la lista de contactos que quedan en la base de datos y la mostramos 
        List<Contacto> listaContactos = contactosDAO.obtenListaContactos();  
        System.out.println("Hay " + listaContactos.size() + "contactos en la base de datos");  

        for(Contacto c : listaContactos) 
        { 
            System.out.println("-> " + c.getNombre()); 
        } 
    } 
}




En el momento que ejecutemos la aplicación Hibernate creará la tabla correspondiente en la base de datos (llamada "contactos") y que tiene los siguientes campos:



Como podemos ver la tabla y los nombres de las columnas se generan en base al archivo de mapeo. En donde hemos colocado los nombres de las columnas se han usado estos nombres, en donde no lo hemos hecho se han tomado los mismos nombres que el de los atributos que estamos mapeando.

Si observamos la salida de la consola podemos ver, que en la parte final, se indica esta creación y además las consultas que Hibernate ha generado (revueltas con los mensajes que sacamos a consola). He marcado con rojo los mensajes que mandamos a consola para que sea fácil distinguirlos.



Segun indica la salida en la base de datos solo debemos tener dos entidades, con los nombres "Nuevo Contacto 2" y "Contacto 3". Revisemos la tabla para ver que esto sea correcto:


Como podemos ver, esto es correcto, en la base de datos existen solo los registros indicados, con lo que podemos estar seguros de que nuestra aplicación funciona!!! ^-^.

Entonces, vimos que con Hibernate fue muy simple hacer la manipulación de los datos de una base de datos relacional sin escribir una sola línea de SQL. En aplicaciones de tamaño mediano a grande esto nos ahorrará mucho tiempo (que normalmente usaríamos en escribir el SQL para query, armar algunas de estas de forma dinámica, pasar los parámetros adecuados, recuperar los mismos de un ResultSet, entre otras cosas) para mejorar algunos otros aspectos de la aplicación, como la interfaz gráfica, la lógica del negocio, o realizar más pruebas. Además nuestro código es más simple y por lo tanto fácil de mantener.

Lo único malo es que ahora tenemos que crear mapeos de las entidades con las tablas de la base de datos en un archivo XLM. Es por esto que en el siguiente tutorial veremos cómo realizar este mismo ejemplo pero usando anotaciones en vez del archivo de mapeo ^-^.

Espero que esto les haya sido de utilidad. No olviden dejar sus comentarios, dudas, y sugerencias.

Saludos.

Descarga los archivos de este tutorial desde aquí:

Entradas Relacionadas: