Mapeos en Hibernate usando una Custom Generator Class para manejar el ID

En realidad el título de esta entrada debería ser:

Como manejar el ID usando una Custom Generator Class, cuando el ID es generado por un trigger (y esto no puede ser cambiado, porque trabajamos con un esquema legacy con el cual debemos convivir).

pero "dicen" que era muy largo ;-)

El caso es que hace aproximadamente un año escribimos una entrada acerca de como afrontar el problema de mapear una entidad con Hibernate, cuando el ID es generado por un trigger (que a su vez utiliza una secuencia) y NO PODEMOS CAMBIAR ESTE HECHO. Es decir, es un esquema legacy con el que, por una u otra razón de orden superior, tenemos que convivir sin alterarlo.

Durante este tiempo algunos colegas me han comentado que en aquel post no quedaba suficientemente claro cual era la situación que queremos solventar. Ya que en principio, con este simple mapeo

<class name="eu.albertomorales.hibernateIntro.model.impl.PersonImpl" table="PERSONAS">
    <id name="id" type="integer" >
        <column name="ID" />
    </id>
    <property name="name" column="NOMBRE" />
</class>

y utilizando el sucio-guarro-penoso truco de asignar un id provisional a la entidad, podemos realizar el guardado sin problema. Ya sea dando valores por defecto a los atributos id de la clase

private Integer id = -1;

o realizando un downCast (ya que setId no estará expuesto en la interfaz de la entidad) y asignando el mencionado valor provisional al id:

((PersonImpl)newPerson).setId(-1);

De esta forma la entidad se guarda, el trigger asigna el id definitivo (mediante el uso se la secuencia correspondiente) y cuando posteriormente la entidad es recuperada (por medio de otros criterios, obviamente no por ID porque lo desconocemos) podemos consultar el ID asignado.

¿Sin problemas?

Este enfoque desenfoque tiene bastantes contras:

  • el ID no puede ser consultado hasta después de que se hace commit (hablando con propiedad hasta que se hace flush). Esto nos ensucia bastante las implementaciones, ya que si estamos haciendo DDD y tenemos manejo de transacciones declarativo (cómo me gusta, por cierto!), no deberíamos tener código del tipo commit ni flush.
  • nos está obligando a asignar valores ficticios temporales (bad smell)
  • en un supuesto bastante común, si el equals de la entidad está basado en el ID podemos tener dos objetos que "deberían ser el mismo" pero no lo son, porque uno tiene el ID ficticio y otro el real. Hay que tener mucho cuidado de deshacernos del primero y no liarnos...sobre todo el objeto puede llegar a ser agregado a un mapa :-( Ya sé que hay mucha literatura acerca de la inconveniencia de utilizar este tipo de IDs en entidades, pero ese no es el tema ahora.

La solución

Fué bien documentada por Jean-Pol Landrain ( @jplandrain ) en diciembre 2004 Before Insert Trigger and ID generator pero no era fácil de encontrar. La probamos y funcionó perfectamente.

Como podemos ver en el video, con el mapeo simple de tipo integer, el ID se desconoce.

En cambio, con la clase custom conocemos el ID desde el momento que hacemos la llamada a .saveOrUpdate(entity); sin haber hecho commit ni flush. Y además (de regalo, como no podía ser de otra forma) los dos objetos, el guardado y el recuperado son el mismo, no solo desde el punto de vista del equals, sino digo que son "el mismo-el mismo", es decir, el mismo id de objeto, puesto que se encontraba en la cache de hibernate tras haberlo guardado.

En su día publicamos el código para Hibernate 3.6.3, ahora publicamos éste actualizado para Hibernate 4.2.1.

package eu.albertomorales.hibernateIntro.persistency.dao.core.hibernate;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.id.AbstractPostInsertGenerator;
import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.id.PostInsertIdentityPersister;
import org.hibernate.id.SequenceIdentityGenerator.NoCommentsInsert;
import org.hibernate.id.insert.AbstractReturningDelegate;
import org.hibernate.id.insert.IdentifierGeneratingInsert;
import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;

/**
* A generator with immediate retrieval through JDBC3
* {@link java.sql.Connection#prepareStatement(String)}. The value of the identity column must be
* set from a "before insert trigger"
* This generator only known to work with newer Oracle
* drivers compiled for JDK 1.4 (JDBC3). The minimum version is 10.2.0.1
* Note: Due to a bug in
* Oracle drivers, sql comments on these insert statements are completely disabled.
*
* This class dos not use the method
* {@link java.sql.Connection#prepareStatement(String, String[]) getGeneratedKeys} because with this
* method the driver wants get the return type form the database meta. If the application user is
* not the shemaowner, this is not possible. This is the reason why the ReturnParameter is hard set
* to Long.
*
* @author Jean-Pol Landrain
* @author Beat Sager
*/
public class TriggerAssignedIdentityGenerator extends AbstractPostInsertGenerator {

    public InsertGeneratedIdentifierDelegate getInsertGeneratedIdentifierDelegate(PostInsertIdentityPersister persister, Dialect dialect, boolean isGetGeneratedKeysEnabled) throws HibernateException {
        return new Delegate(persister, dialect);
    }

    public static class Delegate extends AbstractReturningDelegate {
        private final Dialect dialect;

        private final String[] keyColumns;

        public Delegate(PostInsertIdentityPersister persister, Dialect dialect) {
            super(persister);
            this.dialect = dialect;
            this.keyColumns = getPersister().getRootTableKeyColumnNames();
            if (keyColumns.length > 1) {
                throw new HibernateException("trigger assigned identity generator cannot be used with multi-column keys");
            }
        }

        @Override
        public IdentifierGeneratingInsert prepareIdentifierGeneratingInsert() {
            NoCommentsInsert insert = new NoCommentsInsert(dialect);
            return insert;
        }

        @Override
        protected PreparedStatement prepare(String insertSQL, SessionImplementor session) throws SQLException {
            return session.connection().prepareStatement(insertSQL, keyColumns);
        }

        @Override
        protected Serializable executeAndExtract(PreparedStatement insert, SessionImplementor session) throws SQLException {
            insert.executeUpdate();
            ResultSet generatedKeys = insert.getGeneratedKeys();
            return IdentifierGeneratorHelper.getGeneratedIdentity(generatedKeys, keyColumns[0], getPersister().getIdentifierType());
        }

    }
}

Y ésta sería la manera de utilizarlo en nuestro archivo de mapeo:

<class name="Entity_name" table="Table_name"> 
    <id name="ID"><generator class="eu.albertomorales.hibernateIntro.persistency.dao.core.hibernate.TriggerAssignedIdentityGenerator" /></id> 
    ... 
    <property name="name"/> 
    ... 
</class>

¿Te gustaría compartir con nosotros algún contenido? ¿Tienes algo interesante, y te gustaría compartirlo con la comunidad? Si es así, o estás interesado en recibir más información, por favor déjanos un comentario para que podamos escribir sobre ello... Estamos en contacto!

Y como siempre, si te ha gustado esta entrada, puedes twitearla. Gracias.