miércoles, 4 de enero de 2012

Hibernate y NHibernate (II) Objetos y sesión

En entradas anteriores de NoCompila.com:
  •  Estudiamos como Hibernate y NHibernate es capaz de decidir si debe hacer un Insert o un Update de un objeto. Esta decisión la toma en base al id. Después de escribir el post y revisando la documentación, he descubierto (N)Hibernate también puede utilizar una propiedad version para decidir entre Insert y Update.
  • Vimos que Hibernate y NHibernate se comportan igual, o prácticamente igual, a la hora de generar insert o updates.
Después de unas cuantas charlas con compañeros he decido ampliar el post y hablar sobre la sesión y los objetos, creo que es un tema fundamental para entender el funcionamiento del ORM.
Como lo que vamos a ver son cosas básicas que son iguales en java y en .net solo voy a poner el código en c#, además voy a utilizar unas versiones antiguas (c# 2.0 y NHibernate 2.1) para utilizar métodos y sintaxis lo más comunes y facilite la compresión.
Además vamos a utilizar la clase y el mapeo del post anterior, os los recuerdo:
    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;
        }
    }

  
    
      
    
    
  



La sesión
La sesión de (N)Hibernate es, según la documentación del ORM, una implementación del patrón Unit of Work. La sesión controla todos los cambios que se realizan a los objetos que tiene asociados y sabe si tiene que hacer un delete, update o insert.
Además funciona a modo de caché, ya que al hacer las peticiones de base de datos a través de ella esta puede saber si el objeto ya está cargado en memoria sin necesidad de lanzar una select.
¡¡¡OJO!!! a la sesión no se pueden asociar dos objetos del mismo tipo con el mismo id. de la misma forma que una base de datos no puedes insertar dos registros con la misma pk en la misma tabla.

Estado de los Objetos
Los objetos en (N)Hibernate pueden estar en tres estados:
Transient: es un obejto que no está asociado a una sesión de (N)Hibernate. Es decir cuando creamos un objeto con un new
Persistent: es un objeto que está asociado a una sesión. Al hacer flush() en la sesión, el estado del objeto se envia a base de datos.
Detached: es un objeto asociado a una sesión que ya no existe.
para más información cosultad la documentación oficial


Asociando objetos a la sesión
Hacer que un objeto sea Persistent es relativamente sencillo, como es de imaginar el objeto session nos ofrece varios métodos:
  • save: se utiliza cuando se envía un objeto nuevo a la base de datos. Por ejemplo cuando creamos un objeto que no existe en la base de datos. Lo que hace save es ponerle un id al objeto, según la política que esté definida en el mapeo.
  • update: sirve para asociar un objeto que ya sabemos que existe en base de datos en la sesión. Es decir, a diferencia de save, update no le asigna id al objeto.
  • SaveOrUpdate: lo vimos en el post anterior, asocial al objeto a la sesión, decidiendo si usar save o update
Veamos un ejemplo:
            Configuration cfg = new Configuration();
            cfg.Configure();
            ISessionFactory sf = cfg.BuildSessionFactory();
            ISession session = sf.OpenSession();

            Entidad e1 = new Entidad("UNO"); //creamos un objeto transient
            session.Save(e1); //el objeto se vuelve Persistent

            session.Flush();//Se lanza un Insert en la base de datos
            session.Close(); //e1 pasa a ser detached 

            session = sf.OpenSession();

            Entidad e2 = new Entidad(); //se crea el objeto transient
            e2.Id = e1.Id;
            e2.Nombre = "DOS";

            session.Update(e2); // e2 se vuelve Persistent

            e2.Nombre = "TRES";

            session.Flush ();
            session.Close(); //e2 pasa a ser detached

            sf.Close();

¿Cuantas querys se lanzan contra la base de datos?¿y en qué momento?
Se lanzan tan solo dos querys contra la base de datos, un insert y un update:
    INSERT 
    INTO
        Entidades
        (Nombre) 
    VALUES
        (@p0);
    select
        SCOPE_IDENTITY();
    @p0 = 'UNO'

    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'TRES', @p1 = 1032

El insert se lanza en la primera sesión que creamos, en la línea 7 al hacer el save. Lo hace aqui porque el generador de id que está definido en el mapeo es native, esto significa que le tiene que pedir a la base de datos el id. Únicamente por esta razón lanza el insert en esa línea. Si subiésemos definido el generador como guid el ORM le asignaría como id un new System.Guid() y no ejecutaría el insert en ese momento.

El update se lanza en la linea 22, al hacer el flush de la segunda sesión.
Es muy importante no confundir session.save y session.update con operaciones de base de datos. Os recuerdo que la sesión es la encargada de realizar las comunicaciones con la base de datos, save y update solo pasan a persistent un objeto. Fijaos en el update del log, el valor de nombre es TRES, pero en el código se hizo update con el nombre valiendo DOS. Esto es porque update solo asoció el objeto a la sesión. Es la sesión al hacer flush la que envía los cambios que tiene controlados a la base de datos.

La sesión tiene una propiedad llamada flushMode donde se define la política de flush. Yo recomendaría hacer un flush manual antes de cerrar la sesión para evitarnos sorpresas inesperadas.

  • Lock: solo se debe usar con objetos detached y sirve para atachearlos a la sesión. ¿entonces cual es la diferencia con update? Cuando se hace update de un objeto detached hibernate siempre lanzará un update a la base de datos (salvo que en mapping se le marque con select-before-update, entonces lanzará una select antes para saber si debe actualizar). Es decir si no sabemos si el objeto ha cambiado desde que se cerró la sesión podemos usar Update, si sabemos que no cambio podemos usar lock.
            Configuration cfg = new Configuration();
            cfg.Configure();
            ISessionFactory sf = cfg.BuildSessionFactory();
            ISession session = sf.OpenSession();

            Entidad e1 = new Entidad("UNO"); 
            Entidad e2 = new Entidad("ALFA");
            session.Save(e1); 
            session.Save(e2);

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

            session = sf.OpenSession();

            e2.Nombre = "BETA";

            session.Update(e1); 
            session.Lock(e2, LockMode.None);

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

            session = sf.OpenSession();

            session.Lock(e2, LockMode.None);
            e2.Nombre = "GAMMA";

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

            sf.Close();
En las líneas 7 y 8 lanza los inserts correspondientes para cada objeto.
En el flush de la linea 21 lanza un update para actualizar el objeto e1, esta operación no sería necesaria porque el objeto no cambió, pero al reasociarlo con update se lanza una actualización contra la base de datos. Fijaos que en la linea 16 hemos actualizado el nombre de la e2, pero al estar detached ninguna sessión capturó el cambio, así que en la linea 21 no se nos lanza un update para este objeto.
En el flush de la linea 29 se lanza un update de la e2 ya que esta vez sí que hemos cambiado el nombre del objeto cuando estaba asociado a la sesión.
Aquí tenéis el log con las querys:
    INSERT 
    INTO
        Entidades
        (Nombre) 
    VALUES
        (@p0);
    select
        SCOPE_IDENTITY();
    @p0 = 'UNO'

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

--Flush Línea 21
    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'UNO', @p1 = 1047

 --Flush Línea 29
    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'GAMMA', @p1 = 1048

  • Merge: Quizás lo describía mejor su antiguo nombre SaveOrUpdateCopy, es decir, busca en la sesión y en base de datos el objeto, si existe lo machaca con el que lo pasamos y devuelve una instancia del objeto asociado a la sesión. Con este método evitamos que la sesión lance un error al intentar asociar un objeto con un id ya asociado a la sesión.
No sé si ha quedado claro, veamos un ejemplo
            Configuration cfg = new Configuration();
            cfg.Configure();
            ISessionFactory sf = cfg.BuildSessionFactory();
            ISession session = sf.OpenSession();

            Entidad e1 = new Entidad("UNO"); 
            Entidad e2 = new Entidad("ALFA");
            e1 = (Entidad)session.Merge(e1);
            e2.Id = e1.Id; 
            e2 = (Entidad)session.Merge(e2);

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

            session = sf.OpenSession();

            e2.Nombre = "BETA";

            session.Merge(e2); 
            
            session.Flush ();
            session.Close();

            sf.Close();

En este caso el merge de la línea 8 provoca un insert, ya que como pasamos a la sesión un objeto sin Id (N)Hibernate va a la base de datos para calcularla. En la línea 10 se machaca el objeto de sesión e1, con el e2 (porque tienen el mismo id) pero no se lanza ninguna query. En la línea 12 en el flush de la primera sesión se realiza un update que cambia el nombre de UNO a ALFA en la base de datos. En la línea 19 en el merge de una nueva sesión se lanza una select, ya que el objeto que se le pasa no está en la sesión y tiene que ir a la base de datos a buscarlo. En la línea 21, el flush provoca un update actualizando el Nombre a BETA
--Línea 8, Merge
    INSERT 
    INTO
        Entidades
        (Nombre) 
    VALUES
        (@p0);
    select
        SCOPE_IDENTITY();
    @p0 = 'UNO'

--Línea 12, Flush
    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'ALFA', @p1 = 1053

--Línea 19, Merge
    SELECT
        entidad0_.Id as Id0_0_,
        entidad0_.Nombre as Nombre0_0_ 
    FROM
        Entidades entidad0_ 
    WHERE
        entidad0_.Id=@p0;
    @p0 = 1053

--Línea 21, Flush
    UPDATE
        Entidades 
    SET
        Nombre = @p0 
    WHERE
        Id = @p1;
    @p0 = 'BETA', @p1 = 1053

Espero que con esto quede claro como se comporta NHibernate, está claro que esto no cubre todos los casos, pero creo que es interesante y necesario conocer como se comportan los objetos con la sesión y como se pasan de transient a persistent y a detached.

No hay comentarios:

Publicar un comentario