lunes, 2 de enero de 2012

Hibernate y NHibernate

El otro día en la oficina estaba hablando con un compañero sobre Hibernate. Este compañero me comentaba que debía asociar a la sesión objetos antes de guardarlos. Según me explicaba, al hacer un new de un objeto y guardarlo directamente con Hibernate, el ORM daba fallos en algunos escenarios pudiendo lanzar inserts cuando debería hacer updates o al revés (no me acuerdo).
Desgraciadamente para mí, nunca he podido utilizar NHibernate en un proyecto real de producción, pero estaba seguro que el comportamiento que me describía no se daba en .Net y siendo proyectos hermanos, supuse que el comportamiento sería igual en Hibernate.
Este compañero de la oficina es difícil de convencer, así que programé una mini aplicación de consola en c# para demostrarle como se comportaba el ORM.

Primero hice una clase Entidad:

    class Entidad
    {
        private int _id;
        private string _nombre;

        virtual public int Id
        {
            get { return _id; }
            set { _id = value; }
        }

        virtual public string Nombre
        {
            get {return _nombre;}
            set { _nombre = value; }
        }

        public Entidad()
        {
        }
        public Entidad(string nombre)
        {
            Nombre = nombre;
        }
 
    }

Depues añadí el Mapping de NHibernate

  
    
      
    
    
  

Y por último el código del programa. Tened en cuenta que es una aplicación de NHibernate donde se trabaja directamente con session, es decir, que este código no es una buena referencia para escribir una aplicación profesional.

    class Program
    {
        static void Main(string[] args)
        {
            Configuration cfg = new Configuration();
            cfg.Configure();
            ISessionFactory sf = cfg.BuildSessionFactory();
            ISession session = sf.OpenSession();

            Entidad entidad = new Entidad("PRUEBA");
            session.SaveOrUpdate(entidad);

            session.Flush();
            session.Close();

            session = sf.OpenSession();
            Entidad entidad2 = new Entidad();
            entidad2.Id = entidad.Id;
            entidad2.Nombre = "MODIFICADO";

            session.SaveOrUpdate(entidad2);

            session.Flush();
            session.Close();

            sf.Close();      
        }
    } 

Como podéis ver, el código es muy simple. Se crea una entidad y se llama directamente al método SaveOrUpdate y cerramos la sesión. Si os fijáis en el mapeo de Entidad podéis ver que el generador del id es native, esto significa que la base de datos es la responsable de asignarle un ID. En mi caso (un SQL Server) la columna Id de la tabla Entidades está definida como identity (autoincremental). Mirando la consulta de base de datos que ejecuta NHibernate podemos ver lo siguiente:

INSERT 
    INTO
        Entidades
        (Nombre) 
    VALUES
        (@p0);
    select
        SCOPE_IDENTITY();
    @p0 = 'PRUEBA'

Podemos ver claramente como NHibernate no envía el Id y deja a la base de datos asignarle un valor, despés de insertar el registro, recupera el id haciendo select SCOPE_IDENTITY
Después de eso se cierra la sesión y se crea otra nueva, además se crea una nueva entidad que copia las propiedades Id y Nombre del objeto anterior y se vuelve a guardar. Esta vez nos encontramos con un objeto creado fuera de la sesión de NHibernate, pero que representa a un registro existente en base de datos. ¿Qué hará el ORM cuando tenga que almacenarlo en la base de datos? ¿Sabrá hacerlo? Esto es lo que hace NHibernate:

UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'MODIFICADO', @p1 = 1

NHibernate ha sabido que tenía que hacer un update aunque el objeto no llegó de la sesión. ¿Cómo lo hace? Muy sencillo, en el mapeo se indica que el tipo de generador de id es native, lo que significa que será la base de datos la responsable de asignar id. En el primer caso, como el objeto no tenía id, hizo un insert, en el segundo como ya tenía id hizo un update.

Como comenté anteriormente, mi compañero no es fácil de convencer, y no le gustó mi explicación. Así que intentó ponerla en duda. Me pidió que quitase el identity a la base de datos y que asignase el id de forma manual.
No voy a volver a copiar y pegar todo el código, porque es el mismo, solo os indicaré que puse esta línea antes del primer SaveOrUpdate() esto:
    entidad.Id = 1024;
además modifiqué el mapping
  
    
      
    
    
  

Esta vez, al asignar de forma manual el id, NHibernate no sabe si tiene que lanzar un update o un insert ¿Cómo pensais que se comportaría al guardar el primer objeto?
    SELECT
        entidad_.Id,
        entidad_.Nombre as Nombre0_ 
    FROM
        Entidades entidad_ 
    WHERE
        entidad_.Id=@p0;
    @p0 = 1024

    INSERT 
    INTO
        Entidades
        (Nombre, Id) 
    VALUES
        (@p0, @p1);
    @p0 = 'PRUEBA', @p1 = 1024

Como podemos ver NHibernate no hace magia, lanza un select contra el Id para saber si el objeto existe en base de datos. Como no existe lanza un insert. Con el segundo objeto hace lo mismo:
    SELECT
        entidad_.Id,
        entidad_.Nombre as Nombre0_ 
    FROM
        Entidades entidad_ 
    WHERE
        entidad_.Id=@p0;
    @p0 = 1024

    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'MODIFICADO', @p1 = 1024

Esta vez, después de comprobar que el objeto existe en base de datos mediante otro select, el ORM decide lanzar un update.
Después de todo esto pesaréis que la discusión quedó zanjada, y que mi compañero confía en mi explicación... pues de eso nada. Mi compañero ahora sostiene que Hibernate y NHibernate se comportan de forma diferente, lo que me obliga a hacer el mismo programa en java para demostrarle que Hibernate y NHibernate (al menos en este caso) se comportan de la misma forma.
Así que volví a crear la clase entidad (perdonad si me cargo alguna convención de java, al final, el 90% de mi tiempo lo paso picando vb.net):
public class Entidad {
 private Integer id;
 private String Nombre;
 public void setId(Integer id) {
  this.id = id;
 }
 public Integer getId() {
  return id;
 }
 public void setNombre(String nombre) {
  Nombre = nombre;
 }
 public String getNombre() {
  return Nombre;
 }
}
El mapeo de Hibernate es prácticamente igual que el de NHibernate y este post ya es demasiado largo, así que os copio el código del programa de consola:
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  Configuration conf = new Configuration().configure();
  SessionFactory sf = conf.buildSessionFactory();
  Session session = sf.openSession();
  
  Entidad entidad = new Entidad();
  entidad.setNombre("Prueba");
   
          session.saveOrUpdate(entidad);

          session.flush();
          session.close();
         
          session = sf.openSession();
          Entidad entidad2 = new Entidad();
          entidad2.setId(entidad.getId());
          entidad2.setNombre("MODIFICADO");

          session.saveOrUpdate(entidad2);

          session.flush();
          session.close();

          sf.close();
  
 }

Como podeis ver en java he hecho lo mismo que en c#. La primera vez que se ejecuta el código la base de datos tiene configurado el campo id con identity y las consultas que lanza Hibernate son:
insert into Entidades (Nombre) values (?)
update Entidades set Nombre=? where Id=?

Y al quitar el identity y poner assigned en el mapeo la aplicación genera estas consultas:
select entidad_.Id, entidad_.Nombre as Nombre0_ from Entidades entidad_ where entidad_.Id=?
insert into Entidades (Nombre, Id) values (?, ?)
select entidad_.Id, entidad_.Nombre as Nombre0_ from Entidades entidad_ where entidad_.Id=?
update Entidades set Nombre=? where Id=?

En resumen, y obviando la diferencia de formatos de logs entre Hibernate y su hermano de .NET, los dos ORMs se comportan de la misma forma. (N)Hibernate utiliza el ID para saber si un objeto está en base de datos y saber si tiene que lanzar un update o un insert.
Como nota curiosa he de mencionar que para el ejemplo de java he usado Hibernate 4 y me he llevado una sorpresa al descubrir que buildSessionFactory() está deprecado pero esto no afectó a mi prueba de concepto.

2 comentarios:

  1. y ya puestos a experimentar como se comportaría si la columna id no fuese clave única (ni identity por ende)? daría un pete?

    ResponderEliminar
  2. Para que Hibernate funcione es necesario que las entidades root tengan siempre una clave única.
    Aunque no es recomendable, pueden ser claves compuestas.

    Es decir que si la propiedad Id de Entidad no fuese PK, necesitaríamos otra propiedad para representar la PK de base de datos.

    ResponderEliminar