Mostrando las entradas con la etiqueta NHibernate. Mostrar todas las entradas
Mostrando las entradas con la etiqueta NHibernate. Mostrar todas las entradas

NHibernate y el patrón State

Este es un ejemplo de como persistir con NHibernate una propiedad cuyo tipo de dato es otra clase no persistente, en este caso el Estado de un Evento.

El dominio

Domain

En este dominio el único objeto que se persiste es Evento, el mismo tiene una propiedad Estado de tipo EventoState que es una clase abstracta cuyas posibles implementaciones son las que se ven heredando de esta.

Para el caso del estado no seteado contamos con la clase EventoNullState que tiene el comportamiento correspondiente a este estado del evento:

public virtual EventoState Estado
{
    get { return _estado ?? EventoNullState.GetInstance(); }
    set { _estado = value; }
}

Objetivo

Persistir en una columna de la tabla Evento el Estado que tiene el objeto y que cuando este sea recuperado de la base de datos se cargue el Estado correspondiente.

Persistencia

Para configurar NHibernate utilizamos ConfOrm de la siguiente manera:

var orm = new ObjectRelationalMapper();
orm.TablePerClass<Evento>();

orm.Complex<Evento>(e => e.Estado);

var mapper = new Mapper(orm);
mapper.AddPropertyPattern(p => p.DeclaringType == typeof(Evento) && p.Name == "Estado", a => a.Type<EventoStateType>());

como podemos observar, definimos un patrón para la propiedad Estado diciendo que utilice una instancia de tipo EventoStateType para pasar de objeto a base de datos o viceversa. En esta clase estamos extendiendo la clase GenericWellKnownInstanceType que podemos referenciar desde unhaddins o simplemente tomarla “prestada” (haga clic aquí para ver la clase original) ya que esta clase no depende de otras.

EventoStateType

public class EventoStateType : GenericWellKnownInstanceType<EventoState, string> 
{
    public static IEnumerable<EventoState> All
    {
        get
        {
            return new[]
                   {
                       EventoNullState.GetInstance(), EventoPropuestoState.GetInstance(), EventoAgendadoState.GetInstance(),
                       EventoConfirmadoState.GetInstance(), EventoPublicadoState.GetInstance(), EventoCanceladoState.GetInstance(),
                       EventoDescartadoState.GetInstance()
                   };
        }
    }

    public EventoStateType()
        : base(All, (state, id) => state.Descripcion == id, state => state.Descripcion)
    {
    }

    public override SqlType[] SqlTypes
    {
        get { return new[] {SqlTypeFactory.GetString(25)}; }
    }
}

En el constructor de EventoStateType podemos observar la llamada al constructor de GenericWellKnownInstanceType al que le pasamos los siguientes valores:

    All Una lista con todos los posibles estados.
    (state, id) => state.Descripcion == id una función que será evaluada para relacionar el objeto (state) con su identificador en la base de datos (id).
    state => state.Description una función que, dado un objeto (state), devuelva el identificador con que será persistido.

Y para terminar, en la tabla donde se persiste cada Evento vamos a necesitar un campo con el tipo correspondiente a un string de 25, según SqlTypeFactory.GetString(25) en el cual se persistirá el valor de pa propiedad Descripcion de cada estado.

El código completo podés verlo en el proyecto que estamos desarrollando en la comunidad alt.net hispano para la gestión administrativa de eventos haciendo clic aquí.

Domain sin Ids (pero con ConfOrm)

Preparando un ejemplo para una nueva VAN de alt.net hispano en la que, al igual que la VAN anterior, vamos a usar ConfOrm me encontré con algo curioso, ¡¿ mi objectos de dominio no tienen Ids pero mis tablas si ?!

Siempre hablamos de que los Id de los objetos (PK a nivel de tablas) no deben ser datos significativos para el usuario por varias razones que no vamos a discutir ahora. Siempre decimos que los Id son para el sistema pero hoy me encontré habiendo generado un Domain sin Id, podría decirse: “los Id son para la base de datos”.

Las clases del Domain son las siguientes:

public class Cliente
{
  private readonly IList<Extraccion> _extracciones;
 
  public Cliente()
  {
    _extracciones = new List<Extraccion>();
  }
 
  public virtual string Nombre { get; set; }
  public virtual Cuenta Cuenta { get; set; }
 
  public virtual IEnumerable<Extraccion> Extracciones
  {
    get { return _extracciones; }
  }
 
  public virtual bool RetirarPlata(int importe)
  {
    if (Cuenta != null && Cuenta.Saldo > importe)
    {
      Cuenta.Saldo -= importe;
      Extraccion extraccion = new Extraccion { Fecha = DateTime.Now, Importe = importe };
      _extracciones.Add(extraccion);
      extraccion.Cliente = this;
      return true;
    }
    return false;
  }
}
 
public class Extraccion
{
  public virtual Cliente Cliente { get; set; }
  public virtual DateTime Fecha { get; set; }
  public virtual int Importe { get; set; }
}
 
public class Cuenta
{
  public virtual int Saldo { get; set; }
  public virtual int Version { get; set; }
}

Los Mappings los generamos con ConfOrm:

ObjectRelationalMapper orm = new ObjectRelationalMapper();
 
orm.TablePerClass<Cliente>();
orm.TablePerClass<Cuenta>();
orm.TablePerClass<Extraccion>();
orm.Cascade<Cliente, Cuenta>(Cascade.All);
orm.VersionProperty<Cuenta>(c => c.Version);
 
var mapper = new Mapper(orm);
mapper.Customize<Cliente>(c => c.Property(p => p.Nombre, m => m.Unique(true)));
mapper.Customize<Cliente>(c => c.Property(p => p.Nombre, m => m.NotNullable(true)));
 
HbmMapping mapping = mapper.CompileMappingFor(typeof(Cliente).Assembly.GetTypes());
return mapping;

Si queremos escribir los mappings como xml a partir de la información generada por ConfOrm podemos, el código sería algo como:

var setting = new XmlWriterSettings { Indent = true };
var serializer = new XmlSerializer(typeof(HbmMapping));
 
var fileInfo = new FileInfo("../../mappings.hbm.xml");
 
using (TextWriter sw = new StreamWriter(fileInfo.FullName))
{
  var xw = XmlWriter.Create(sw, setting);
  serializer.Serialize(xw, mapping);
}

y el resultado:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" assembly="EjemploObtenerXmlDesdeConfOrm" xmlns="urn:nhibernate-mapping-2.2">
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Cliente">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <property name="Nombre" unique="true" />
    <many-to-one name="Cuenta" cascade="all" />
    <bag name="Extracciones" access="field.camelcase-underscore" inverse="true" cascade="all,delete-orphan">
      <key column="Cliente" on-delete="cascade" />
      <one-to-many class="EjemploObtenerXmlDesdeConfOrm.Domain.Extraccion" />
    </bag>
  </class>
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Extraccion">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <many-to-one name="Cliente" />
    <property name="Fecha" />
    <property name="Importe" />
  </class>
  <class name="EjemploObtenerXmlDesdeConfOrm.Domain.Cuenta">
    <id type="Guid">
      <generator class="guid.comb" />
    </id>
    <version name="Version" />
    <property name="Saldo" />
  </class>
</hibernate-mapping>

Recién en los mappings aparecen los Ids (pero sin tener propiedad en el objeto), quizás para muchos es algo de su día a día, pero en mi caso fue la primera vez que me doy cuenta de esto.

El DER de la base de datos, generada con SchemaExport, es:

DER

Gracias ConfOrm por seguirme enseñando sobre mappings, ORMs y OOP en general.

Con este blog-post no intento decir que es una buena práctica, seguramente que buscar nuestros objetos en la db por valores numéricos es mucho mas performante que por los datos significativos para el usuario como puede ser el nombre en este caso, pero si me hace reflexionar sobre el uso y abuso de los Ids.

Cómo no odiar a la session de NHibernate

6 pasos para llega a odiar la session de nh

Primer paso: pensar y usar la session de nh (de ahora en mas simplemente ‘session’) como si sólo fuese la una conexión a base de datos, es decir que la abrimos, la usamos y la cerramos.

Segundo paso: cuando empiezan las “LazyInitializationException” ponemos los lazy en false en un intento de que no lance más esa odiosa excepción.

Tercer paso: cuando queremos actualizar objetos empezamos a recibir “PersistentObjectException: detached entity passed to persist” o “HibernateException: reassociated object has dirty collection”. y aquí es donde tendemos a dejar una session abierta y usarla en toda la aplicación. Quizás no nos damos cuenta, pero ya podríamos volver a poner el lazy en true.

en este momento bastaría con poner “LazyInitializationException” o “PersistentObjectException: detached entity” o “NHibernate reassociated object has dirty collection” en google para saber que no estamos solos, ni somos los primeros en encontrarnos con este problema.

Cuarto paso: si seguimos con los lazy en false, vamos a tener problemas de performance cuando el volumen de datos sea considerable (afectando así a la escalabilidad), vamos a pedir una factura (para consultar la fecha por ejemplo) y nos va a cargar las líneas de la misma, sus tracks, su álbum, su artista, los otros tracks del álbum, etc. Aquí es donde decimos que “NHibernate no sirve para aplicaciones con un dominio complejo” o “no sirve para grandes aplicaciones como la nuestra”.

Quinto paso: si en algún momento optamos por una session dejándola abierta, se vuelve a complicar cuando la aplicación corre en múltiples hilos, como cuando un sitio web entra en testing ya que hay usuarios concurrentes, cosa que no pasaba en el ambiente de desarrollo local de cada PC. Aquí ya estamos “complicados”.

Sexto extra: como frutilla del postre, luego de que la session nos lanza una excepción de, por ejemplo, validación, notamos que la session empieza a tener un comportamiento extraño. No nos guarda los objetos que le decimos que nos guarde. Aquí llegamos al odio!

Algunos otros síntomas también pueden ser el uso de Session.Evict, Session.Clear, Session.Flush por todo nuestro código. Aquí se declaró la guerra!

Posiblemente pensemos que no tenemos idea que es la session, y quizás sea verdad, quizás no tengamos idea de que es la session. Aquí empiezan las soluciones, empezamos a buscar en google como se debe manejar la session de NH, encontramos algunos frameworks que lo hacen y un montón de post al respecto de este tema, quizás así llegaste a este.

Empezando el camino de la reconciliación

Lo primero que tenemos que hacer es entender qué es esta session. Cuando más o menos entendamos esto, vamos a poder decidir como manejarla. Sino vamos a estar pensando que manejamos un auto y estamos piloteando a un avión (con 200 pasajeros a bordo) y nos vamos a dar cuenta cuando estemos carreteando.

Algunos tips sobre que es la session:
- la session tiene información del estado original de los objetos, que luego va a usar para saber si los mismos fueron modificados.
- la session tiene una caché que nos garantiza que existe sólo una instancia de cada objeto dentro de ésta, y que no a volver a buscar en la base de datos un objeto que ya consultó a menos que se lo indiquemos específicamente.
- la session va a volver a ser usada cuando se carguen los lazy loading (ver ejemplo más abajo).

Podemos pensar a la session de nh como una conversación entre los objetos de nuestra aplicación y nuestra base de datos. Esta conversación puede durar lo que una llamada a un método, lo que un request dentro de un entorno web o estar asociada al ciclo de vida de otro objeto, etc. Lo importante en este punto es entender que el ciclo de vida de la session de nh depende del contenido de la conversación, no podemos cortar la conversación, seguir hablando y pretender entendernos. Si queremos introducir un objeto a una conversación (session) diferente a la que lo originó hay técnicas especificas para esto (reattach). Así que o trabajamos con la misma session o hacemos un reattach.

Si logramos entender este concepto, vamos a saber cuanto tiene que durar nuestra conversación, vamos a entender el ciclo de vida que debe tener nuestra session. Y aquí es donde podemos optar por algún framework que lo haga por nosotros, pero siempre le vamos a tener que decir qué ciclo de vida queremos que tenga en cada caso.

framworks que administran la session por nosotros: los mismos Context de NHibernate (como WebSessionContext y ThreadStaticSessionContext), unhaddinsNHibernate.Burrow, Castle, Spring.NET, Rhino Tools, etc.

Entendiendo el Lazy Loading con ejemplos

   1: Invoice invoice;
   2: using (ISession session = sessionFactory.OpenSession())
   3: {
   4:     invoice = session.Get<Invoice>(id);
   5:     Assert.IsNotNull(invoice);
   6:  
   7:     Assert.IsNotNull(invoice.Customer);
   8:     Assert.Greater(invoice.Customer.Id, 0);
   9: }
  10:  
  11: string firstName = string.Empty;
  12: Assert.Throws<LazyInitializationException>(() => firstName = invoice.Customer.FirstName);
  13:  
  14: Assert.IsEmpty(firstName);




como podemos ver en este test, en la línea 9 estamos cerrando la session (el dispose del using) y en la línea 12 estamos confirmando una excepción (de tipo LazyInitializationException) al intentar acceder a la propiedad FirstName de la propiedad Customer de Invoice. Esto significa que la propiedad Customer no fue cargada aún (de la base de datos) y necesitaba la session de nh para hacerlo, la misma session con que fue cargado el objeto. Además, como pueden observar, en las líneas 7 y 8 estamos haciendo uso de esta propiedad, pero como no es necesario (todavía) ir hasta la base de datos por estos datos es que nhibernate no los carga.

¿cuál sería el ciclo de vida correcto para este ejemplo?


   1: using (ISession session = sessionFactory.OpenSession())
   2: {
   3:     Invoice invoice = session.Get<Invoice>(id);
   4:     Assert.IsNotNull(invoice);
   5:  
   6:     Assert.IsNotNull(invoice.Customer);
   7:     Assert.Greater(invoice.Customer.Id, 0);
   8:  
   9:     string firstName = invoice.Customer.FirstName;
  10:  
  11:     Assert.AreEqual("Nelo", firstName);
  12: }


El objeto invoice (y los objetos relacionados por sus propiedades) se usan dentro del ciclo de vida de la session.

Les dejo una lectura recomendada (obligatoria diría) aquí.

Persistencia del dominio de nuestro ejemplo

Nuestro dominio de objetos

Domain

Nuestro modelo relacional (tomado de Chinook 1.1)

modelo relacional

¿Cómo lo persistimos?

¿Cómo guardamos y recuperamos nuestro dominio de objetos de la una base de datos?... como se imaginarán no estoy pensando en escribir los insert, update, delete o select necesarios sino que, siguiendo en tren de delegar responsabilidades a expertos, usamos un ORM para la persistencia de nuestras entidades, nuestra primera implementación va a ser con nhibernate (nh). Esto quiere decir que vamos a hacer una implementación de DAOs con nh que se va a llamar DAOs.NH.

¿Cómo hacemos para no quedar “atados” a NHibernate?

Sólo vamos a usar (referenciar) nhibernate desde las implementaciones correspondientes (al momento de este post DAOs.NH, DAOs.NH.Castle y BEs.Validations.NHV). Es decir que no vamos a tener referencias a Nhibernate desde otras implementaciones como Models.Impl o Services.Impl. Ni decir que el Core de nuestra aplicación no va a conocerlo (esto es, los contratos, las BEs y los DTOs).

Para ver como usar NHibernate de la forma tradicional (hasta el momento) pueden ver la documentación oficial o los muchos ejemplos que hay en internet.

Configurando NH con ConfOrm

En este post vamos a ver la configuración de NHibernate mediante ConfOrm para nuestro dominio, y para esto vamos a hacer algunos ajustes a la base de datos (db). Es verdad que también podríamos indicar en los mappings (o a ConfOrm) las diferencias entre el modelo de objetos y el modelo relacional, pero como considero que la base de datos también es parte de nuestra aplicación y por lo tanto es factible de refactorización es que decido modificarla. Los scripts para esto están en la carpeta scripts de nuestro código de ejemplo. Dichos cambios consisten en renombrar algunos campos y una tabla, no modificamos en concepto el diseño de la db.

Ahora bien, vamos al código:

ObjectRelationalMapper orm = new ObjectRelationalMapper();

orm.TablePerClass<Album>();
orm.TablePerClass<Artist>();
orm.TablePerConcreteClass<Person>();
orm.TablePerClass<Genre>();
orm.TablePerClass<Invoice>();
orm.TablePerClass<InvoiceLine>();
orm.TablePerClass<MediaType>();
orm.TablePerClass<Playlist>();
orm.TablePerClass<Track>();

orm.Bag<Playlist>(pl => pl.Tracks);
orm.ManyToMany<Playlist, Track>();

Mapper mapper = new Mapper(orm);

Type type = typeof(Track);
HbmMapping mapping = mapper.CompileMappingFor(type.Assembly.GetTypes());

Configuration configuration = new Configuration();
configuration.Configure();

configuration.AddDeserializedMapping(mapping, "DMS_BEs");

return configuration;

Como podemos ver, el objetivo de esto es llegar al objeto Configuration de nh, el cual antes configurábamos a partir de los .hbm.xml embebidos en el assembly o con attributes sobre nuestro dominio. Para ver las distinas opciones de configuración de ConfOrm pueden revisar el blog de Fabio que seguro encontrarán mucho.

¿y como quedaron estos mappings?

No es necesario escribir los mappings en un archivo, pero si queremos verlos podemos:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="DMS.BEs" assembly="DMS.BEs" xmlns="urn:nhibernate-mapping-2.2">
    <class name="Invoice">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Customer" />
        <property name="InvoiceDate" />
        <property name="BillingAddress" />
        <property name="BillingCity" />
        <property name="BillingState" />
        <property name="BillingCountry" />
        <property name="BillingPostalCode" />
        <bag name="Lines" inverse="true" cascade="all,delete-orphan">
            <key column="Invoice" on-delete="cascade" />
            <one-to-many class="InvoiceLine" />
        </bag>
    </class>
    <class name="Track">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <many-to-one name="Album" />
        <many-to-one name="MediaType" />
        <many-to-one name="Genre" />
        <property name="Composer" />
        <property name="Milliseconds" />
        <property name="Bytes" />
        <property name="UnitPrice" />
    </class>
    <class name="Artist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Genre">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="MediaType">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
    </class>
    <class name="Playlist">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="Name" />
        <bag name="Tracks">
            <key column="playlist_key" />
            <many-to-many class="Track" />
        </bag>
    </class>
    <class name="InvoiceLine">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Invoice" />
        <many-to-one name="Track" />
        <property name="UnitPrice" />
        <property name="Quantity" />
    </class>
    <class name="Album">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <many-to-one name="Artist" />
        <property name="Title" />
        <bag name="Tracks" inverse="true" cascade="all,delete-orphan">
            <key column="Album" on-delete="cascade" />
            <one-to-many class="Track" />
        </bag>
    </class>
    <class name="Person" abstract="true">
        <id name="Id" type="Int32">
            <generator class="hilo" />
        </id>
        <property name="FirstName" />
        <property name="LastName" />
        <property name="Address" />
        <property name="City" />
        <property name="State" />
        <property name="Country" />
        <property name="PostalCode" />
        <property name="Phone" />
        <property name="Fax" />
        <property name="Email" />
    </class>
    <union-subclass name="Employee" extends="Person">
        <property name="Title" />
        <property name="BirthDate" />
        <property name="HireDate" />
        <many-to-one name="ReportsTo" />
    </union-subclass>
    <union-subclass name="Customer" extends="Person">
        <property name="Company" />
        <many-to-one name="SupportRepresentant" />
    </union-subclass>
</hibernate-mapping>