Archive for the '4752' Category

NHibernate 3 (Parte 7) One-To-Many con Inverse

Monday, May 30th, 2011

Anterior Post
Siguiente Post

En el anterior ejemplo tenía un problema a resolver: por cada capítulo de un libro nuevo se ejecutaba un INSERT Y un UPDATE, cuando bien podría bastar uno. En este post, uso una manera alternativa de mapear una lista uno-a-varios, usando el atribute inverse. Como ya es usual, el código del ejemplo está en mi AjCodeKata Code Project, en el directorio trunk/NHibernate/BookChapters. Pueden bajarse una versión “congelada a hoy” desde NHibernate3BookChaptersInverse.zip.

Modifiqué la anterior solución, agregando un nuevo proyecto de consola Books.Inverse, copiando el proyecto Books.Console original. Entonces, agregué un nuevo atributo en el mapeo de Book:

<list name="Chapters" cascade="all-delete-orphan" 
  inverse="true">
  <key column="BookId"/>
  <index column="ChapterIndex"/>
  <one-to-many class="Chapter"/>
</list>

El nuevo atributo es inverse=”true”. Con esta pista, NHibernate conoce que el manejo de la lista y sus elementos está bajo el control del programador. ¿Qué implica esto? Ejecuté el nuevo programa de consola: cada capítulo es grabado con un solo INSERT, pero el resultado en la base de datos fue:

No hay BookId, no hay ChapterIndex con valores, son todos null! El problema está en el código original:

cookbook.Chapters.Add(new Chapter() { Title = "Models and Mappings" });
cookbook.Chapters.Add(new Chapter() { Title = "Configuration and Schema" });
cookbook.Chapters.Add(new Chapter() { Title = "Sessions and Transactions" });

Cuando creo un nuevo Chapter, no pongo valores en Book o en su Chapter Index. Esos datos los ponía el NHiberante. Pero ahora con inverse=”true”, esos valores caen bajo mi responsabilidad: NHibernate no interviene. Entonces, para solucionar el problema, cambié la clase Chapter, agregando una nueva propiedad:

// Used in Inverse example
public virtual int ChapterIndex { get; set; }

Y cambié el código a:

cookbook.Chapters.Add(new Chapter() { Title = "Models and Mappings", Book = cookbook, ChapterIndex = 0 });
cookbook.Chapters.Add(new Chapter() { Title = "Configuration and Schema", Book = cookbook, ChapterIndex = 1 });
cookbook.Chapters.Add(new Chapter() { Title = "Sessions and Transactions", Book = cookbook, ChapterIndex = 2 });

Ahora, la salida muestra los insert correctos:

NHibernate: INSERT INTO Books (Title, Author, Id) VALUES (@p0, @p1, @p2);@p0 = 
'NHibernate Cookbook' [Type: String (4000)], @p1 = 'Jason Dentler' 
[Type: String (4000)], @p2 = b643aa0e-1917-4066-96a4-64a09562a58a [Type: Guid (0)]

NHibernate: INSERT INTO Chapters (Title, Notes, BookId, ChapterIndex, Id) VALUES
 (@p0, @p1, @p2, @p3, @p4);@p0 = 'Models and Mappings' [Type: String (4000)], @p1 
 = NULL [Type: String (4000)], @p2 = b643aa0e-1917-4066-96a4-64a09562a58a
 [Type : Guid (0)], @p3 = 0 [Type: Int32 (0)], @p4 = 
 df9114af-7183-4b1b-9826-d12982a898a5 [Type: Guid (0)]
 
NHibernate: INSERT INTO Chapters (Title, Notes, BookId, ChapterIndex, Id) VALUES
 (@p0, @p1, @p2, @p3, @p4);@p0 = 'Configuration and Schema' 
 [Type: String (4000)], @p1 = NULL [Type: String (4000)], @p2 =
 b643aa0e-1917-4066-96a4-64a09562a58a [Type: Guid (0)], @p3 = 1 [Type: Int32 (0)],
 @p4 = 3fc1df36-c7de-4ff8-add2-8f843b627115 [Type: Guid (0)]

NHibernate: INSERT INTO Chapters (Title, Notes, BookId, ChapterIndex, Id) VALUES
 (@p0, @p1, @p2, @p3, @p4);@p0 = 'Sessions and Transactions' 
 [Type: String (4000)], @p1 = NULL [Type: String (4000)], @p2 = 
 b643aa0e-1917-4066-96a4-64a09562a58a  [Type: Guid (0)], @p3 = 2 [Type: Int32 (0)],
 @p4 = 9c8891e4-6419-4469-aa3a-154f2c908985 [Type: Guid (0)]

Los datos grabados están bien:

Todo bien! Noten que no tuve que agregar los capítulos a la sesión de NHibernate: son grabados como parte del libro nuevo, igual que en el anterior ejemplo. Pero con inverse=true, tuve que poner los valores de la relación inversa (de Chapter a Book) y la propiedad de orden que había elegido.

Próximos pasos: explorar borrado y actualización, mapeos más complejos, otras opciones de logging, otras formas de hacer mapeos (como Fluen NHibernate, ConfORM, y el nuevo mapeo por código de NHibernate 3.2).

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

NHibernate 3 (Parte 6) One-To-Many con Many-To-One

Monday, May 23rd, 2011

Anterior post
Próximo post

Esta vez, la base de datos es la misma del anterior ejemplo (mismo nombre y scripts de creación):

Pero quiero tener una referencia, en el dominio, de Chapter (capítulo) a Book (libro):

public class Chapter
{
    public virtual Guid Id { get; set; }
    public virtual string Title { get; set; }
    public virtual string Notes { get; set; }
	
	// New property
    public virtual Book Book { get; set; } 
}

Ass que cambié el mapeo de Chapter a:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Books"
namespace="Books">
  <class name="Chapter" table="Chapters">
    <id name="Id">
      <generator class="guid" />
    </id>
    <property name="Title" not-null="true" />
    <property name="Notes" />
    <many-to-one name="Book" column="BookId" />
  </class>
</hibernate-mapping>

El nuevo elemento es many-to-one. El mapeo de Book no tiene cambios:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Books"
namespace="Books">
  <class name="Book" table="Books">
    <id name="Id">
      <generator class="guid" />
    </id>
    <property name="Title" not-null="true" />
    <property name="Author" not-null="true"/>
    <list name="Chapters" cascade="all-delete-orphan">
      <key column="BookId"/>
      <index column="ChapterIndex"/>
      <one-to-many class="Chapter"/>
    </list>
  </class>
</hibernate-mapping>

Agregué el “show_sql” con valor “true” en el archivo de configuración, en la sección de NHibernate:

  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
      <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.connection_string">Data Source=.\SQLEXPRESS;Initial Catalog=NHibernate3BooksOneToMany;Integrated Security=True</property>
      <property name="proxyfactory.factory_class">
        NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
      </property>
     <property name="show_sql">true</property>
      <mapping assembly="Books.Console" />
    </session-factory>
  </hibernate-configuration>

Esta propiedad es muy útil para aprender qué hace el NHibernate por debajo: muestra en consola los comandos SQL que NHibernate ejecuta durante su operación. Este es programa de consola:

ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
using (ISession session = sessionFactory.OpenSession())
{
    using (ITransaction tx = session.BeginTransaction())
    {
        Book cookbook = new Book()
        {
            Title = "NHibernate Cookbook",
            Author = "Jason Dentler",
            Chapters = new List<Chapter>()
        };
        cookbook.Chapters.Add(new Chapter() { Title = "Models and Mappings" });
        cookbook.Chapters.Add(new Chapter() { Title = "Configuration and Schema" });
        cookbook.Chapters.Add(new Chapter() { Title = "Sessions and Transactions" });
        session.Save(cookbook);
        tx.Commit();
        session.Close();
    }
}
using (ISession session = sessionFactory.OpenSession())
{
    foreach (Book book in session.Query<Book>().Fetch(b => b.Chapters))
        {
            System.Console.WriteLine(string.Format("Book {0}", book.Title));
            int nchapter = 0;
            foreach (Chapter chapter in book.Chapters)
            {
                System.Console.WriteLine(string.Format("Chapter {0}:{1}", ++nchapter, chapter.Title));
                System.Console.WriteLine(string.Format("From Book {0}", chapter.Book.Title));
            }
        }
}
System.Console.ReadKey();

Tengo un nuevo método: .Fetch. Lo explicaré más abajo. Ahora, ejecuto el programa, y examino la salida. Al principio aparece:

NHibernate: INSERT INTO Books (Title, Author,
 Id) VALUES (@p0, @p1, @p2);@p0 = 'NHibernate Cookbook' [Type: String (4000)], @p1 = 
'Jason Dentler' [Type: String (4000)], 
@p2 = 20d05308-aea1-49f2-9b4a-7441e73dab4e [Type: Guid (0)]

Es el comando de inserción de los datos de libro. Luego aparece:

NHibernate: INSERT INTO Chapters (Title, Notes, BookId, Id) VALUES (@p0, @p1, @p 
2, @p3);@p0 = 'Models and Mappings' [Type: String (4000)], @p1 = NULL [Type: 
String (4000)], @p2 = NULL [Type: Guid (0)], @p3 = 
15ce4169-08ef-4f75-8493-8fe68da7e918 [Type: Guid (0)] 
NHibernate: INSERT INTO Chapters (Title, Notes, BookId, Id) VALUES (@p0, @p1, @p2, @p3);
@p0 = 'Configuration and Schema' [Type: String (4000)], @p1 = NULL [Type: String (4000)],
 @p2 = NULL [Type: Guid (0)], @p3 = 3edb510d-4cbd-44fd-b4a1-094a258ec07f [Type: Guid (0)] 
NHibernate: INSERT INTO Chapters (Title, Notes, BookId, Id) VALUES (@p0, @p1, @p2,
 @p3);@p0 = 'Sessions and Transactions' [Type: String (4000)], @p1 = NULL [Type: String (4000)], @p2 = NULL [Type: Guid (0)], @p3 = af025d1b-cdc6-4c8f-9014-71c5dee135e1 [Type: Guid (0)]

Estos comandos insertan los tres capítulos. Pero, epa! NO HAY nada que guarde el BookId de nuestro libro (noten que @p2 = NULL) y ni aparece la columna ChapterIndex. Curiosamente, NHibernate pone los valores de esas columnas EN UNA SEGUNDA PASADA:

NHibernate: UPDATE Chapters SET BookId = @p0, ChapterIndex = @p1 WHERE Id = @p2; 
@p0 = 20d05308-aea1-49f2-9b4a-7441e73dab4e [Type: Guid (0)], @p1 = 0 [Type: 
Int32 (0)], @p2 = 15ce4169-08ef-4f75-8493-8fe68da7e918 [Type: Guid (0)] 
NHibernate: UPDATE Chapters SET BookId = @p0, ChapterIndex = @p1 WHERE Id = @p2; 
@p0 = 20d05308-aea1-49f2-9b4a-7441e73dab4e [Type: Guid (0)], @p1 = 1 [Type: 
Int32 (0)], @p2 = 3edb510d-4cbd-44fd-b4a1-094a258ec07f [Type: Guid (0)] 
NHibernate: UPDATE Chapters SET BookId = @p0, ChapterIndex = @p1 WHERE Id = @p2; 
@p0 = 20d05308-aea1-49f2-9b4a-7441e73dab4e [Type: Guid (0)], @p1 = 2 [Type: 
Int32 (0)], @p2 = af025d1b-cdc6-4c8f-9014-71c5dee135e1 [Type: Guid (0)]

Esto es raro (y es la razón por la que en el anterior ejemplo tuve que definir a ChapterIndex y a BookId como columnas que acepten null en la base de datos). Yo hubiera esperado que NHibernate las grabe en una sola pasada, ya en el INSERT de cada capítulo. Pero parece que tiene su propia “opinión y conducta”, y para él lo “natural” es hacerlo en dos pasadas. Esta es una de las MUCHAS conductas que hay que aprender para realmente domina NHibernate. Y no es una conducta evidente. Noten la utilidad de usar show_sql: hay otras opciones de logueo en NHibernate (por ejemplo, log4net), pero pueden comenzar con esta simple propiedad para sus programas iniciales. Debería mejorara esto de DOS pasadas a UNA, en un próximo post.

Este es el comando de Select:

NHibernate: select book0_.Id as Id0_0_, chapters1_.Id as Id1_1_, book0_.Title as 
Title0_0_, book0_.Author as Author0_0_, chapters1_.Title as Title1_1_, chapters 
1_.Notes as Notes1_1_, chapters1_.BookId as BookId1_1_, chapters1_.BookId as Boo 
kId0__, chapters1_.Id as Id0__, chapters1_.ChapterIndex as ChapterI5_0__ from Bo 
oks book0_ left outer join Chapters chapters1_ on book0_.Id=chapters1_.BookId

Sorpresa! El select recupera Books Y Chapters, usando un outer join. De esta manera, NHibernate puede llenar los datos de los capítulos correspondientes a cada libro en un solo comando. Esta no es la conducta asumida: los capítulos no son recuperados cada vez que recuperamos un libro. NHibernate asume que cargar los datos de Chapters SOLO cuando comenzamos a iterar sobre la lista. El comando de arriba, con el join agregado es consecuencia de haber usado el Fetch:

foreach (Book book in session.Query<Book>().Fetch(b => b.Chapters))

Ese método adicional, dice: “Hey, NHibernate! Ve y trae los libros, pero los capítulos también, que ya te aviso que los voy a necesitar!”. Pueden experimentar qué pasa si sacan el Fetch. El programa sigue funcionando, y mostrando los capítulos de cada libro. Pero ahora la salida (parcial) sería (con otros datos):

NHibernate: select book0_.Id as Id0_, book0_.Title as Title0_, book0_.Author as 
Author0_ from Books book0_ 
Book NHibernate Cookbook 
NHibernate: SELECT chapters0_.BookId as BookId1_, chapters0_.Id as Id1_, chapter 
s0_.ChapterIndex as ChapterI5_1_, chapters0_.Id as Id1_0_, chapters0_.Title as Title1_0_,
 chapters0_.Notes as Notes1_0_, chapters0_.BookId as BookId1_0_ FROM Chapters
 chapters0_ WHERE chapters0_.BookId=@p0;@p0 = 
cfb043eb-6ffc-4727-a5ad-05afe867ab57 [Type: Guid (0)] 
Chapter 1:Models and Mappings 
From Book NHibernate Cookbook 
Chapter 2:Configuration and Schema 
From Book NHibernate Cookbook 
Chapter 3:Sessions and Transactions 
From Book NHibernate Cookbook

Hay un SELECT para el libro, y luego, PARA CADA LIBRO hay un select para recuperar sus capítulos. Arriba se muestra un solo libro, pero si tuviéramos 10, tendríamos 10 comandos select de capítulos. De nuevo: otra “conducta” a tener en cuenta para tener un programa que no abuse de enviar comandos a la base de datos. Notemos que la salidad:

Book NHibernate Cookbook

 

APARECE ANTES que el select de los capítulos. Sólo hasta que el programa comienza a iterar por la lista de capítulos de un libro, envía el SELECT correspondiente.

Otro punto: el chapter.Book no está en nulo. Gracias a NHibernate, la referencia de Chapter a Book ha sido puesta de forma automática.

Entonces, agregué el método .Fetch porque se que voy a listar los datos de los capítulos y los necesito. Igualmente, todavía tenemos que explorar alternativas para evitar el par INSERT-UPDATE cuando se da de alta un libro con capítulos, en próximo post.

Como otras veces, el código está en mi AjCodeKata Code Project, en el directorio trunk/NHibernate/BookChapters. Pueden bajara ahora mismoa una versión “congelada” desde my Skydrive: NHibernate3BookChapters.zip.

Próximos pasos: explorar el inverse=”true”, otras opciones de logging, many-to-many, cache de session, etc.

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

NHibernate 3 (Parte 5) Primer mapeo Uno-a-Varios

Tuesday, May 10th, 2011

Anterior post
Próximo post

Esta vez, tengo este modelo de datos para mapear a clases:

Cada Book (libro) tiene 0, 1  o más Chapter (Capítulos). Este es el diagrama de clases de mi dominio:

 

El código para Book.cs:

public class Book { public virtual Guid Id { get; set; } public virtual string Title { get; set; } public virtual string Author { get; set; } public virtual IList<Chapter> Chapters { get; set; } }



Como antes, las propiedades son virtuales (tengo que escribir por qué son virtuales, en un post de esta serie). Y noten, Chapter es un  IList<Chapter>. El IList será armado por NHibernate. El Chapter.cs:

public class Chapter { public virtual Guid Id { get; set; } public virtual string Title { get; set; } public virtual string Notes { get; set; } }



El mapeo de Book tiene un nuevo elemento:

<?xml version=”1.0″ encoding=”utf-8″ ?> <hibernatemapping xmlns=“urn:nhibernate-mapping-2.2” assembly=“Books” namespace=“Books”> <class name=“Book” table=“Books”> <id name=“Id”> <generator class=“guid.comb” /> </id> <property name=“Title” notnull=“true” /> <property name=“Author” notnull=“true”/> <list name=“Chapters” cascade=“all-delete-orphan”> <key column=“BookId”/> <index column=“ChapterIndex”/> <onetomany class=“Chapter”/> </list> </class> </hibernate-mapping>



El elemento <list> apunta a la propiedad de Book llamada Chapters (la IList). Los elementos serán ordenados basado en la columna de table ChapterIndex. BookId y ChapterIndex no son propiedades de la clase Chapter. Son columnas en la tabla Chapter. El mapeo de la clase Chapter no tiene ninguna referencia a Book:

<?xml version=”1.0″ encoding=”utf-8″ ?> <hibernatemapping xmlns=“urn:nhibernate-mapping-2.2” assembly=“Books” namespace=“Books”> <class name=“Chapter” table=“Chapters”> <id name=“Id”> <generator class=“guid.comb” /> </id> <property name=“Title” notnull=“true” /> <property name=“Notes” /> </class> </hibernate-mapping>



 

¿Qué es eso de “delete-all-orphan”? Vean

http://www.nhforge.org/doc/nh/en/index.html#collections-onetomany

El programador puede agregar y remover capítulos de un objeto libro, y entonces, NHibernate se encargará de su persistencia. Si un objeto se remueve de la lista por código, NHibernate eliminará el capítulo removido SI no ha sido agregado mientras tanto a OTRO libro (es decir, quedó como objeto, pero huérfano).

Este es el código para agregar y recuperar un libro con capítulos:

ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory(); using (ISession session = sessionFactory.OpenSession()) { using (ITransaction tx = session.BeginTransaction()) { Book cookbook = new Book() { Title = “NHibernate Cookbook“, Author = “Jason Dentler“, Chapters = new List<Chapter>() { new Chapter() { Title = “Models and Mappings” }, new Chapter() { Title = “Configuration and Schema” }, new Chapter() { Title = “Sessions and Transactions” } } }; session.Save(cookbook); foreach (Book book in session.Query<Book>()) { System.Console.WriteLine(string.Format(“Book {0}“, book.Title)); int nchapter = 0; foreach (Chapter chapter in book.Chapters) System.Console.WriteLine(string.Format(“Chapter {0}:{1}“, ++nchapter, chapter.Title)); } tx.Commit(); session.Close(); } }



Tuve un problema al escribir el ejemplo: inicialmente había definido ChapterIndex y BookId como columnas que no admitían null, en la tabla Chapters. Pero, notablemente, NHibernate requiere que esos campos puedan aceptar valores null. Así que en mis primeras pruebas, me daba un error al grabar un nuevo libro con capítulos. Ahí me di cuenta de esta nota (en la documentación de NHForge, en el enlace que puse más arriba):

Very Important Note: If the <key> column of a <one-to-many> association is declared NOT NULL, NHibernate may cause constraint violations when it creates or updates the association. To prevent this problem, you must use a bidirectional association with the many valued end (the set or bag) marked as inverse=”true”. See the discussion of bidirectional associations later in this chapter.

Cambié las columnas a nullable, y todo funcionó. Me asombra este requirimiento: yo esperaba que NHibernate llene las columnas BookId y ChapterIndex DURANTE la grabación de cada capítulo. Pero nones. Graba primero el capítulo con los campos en null, y luego los actualiza.

El código de este post, como es usual, está en mi AjCodeKatas Google Project, en trunk/NHibernate/BooksOneToMany. Pueden bajar una versión “actual congelada” desde NHibernate3BooksOneToMany.zip.

Próximos pasos: explorar otras opciones uno-a-varios, mapeos bidireccionales, y más.

Fuentes consultadas: Jason Dentler’s NHibernate 3.0 Cookbook
http://ayende.com/Blog/archive/2006/12/02/nhibernatecascadesthedifferentbetweenallalldeleteorphansandsaveupdate.aspx
http://www.nhforge.org/doc/nh/en/index.html#collections-onetomany

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

NHibernate 3 (Parte 4) Tabla Por Clase

Tuesday, April 19th, 2011

Anterior post
Siguiente post

En los anteriores posts (Tabla por Jerarquía, Tabla por Clase Concreta), implementé dos estrategias de mapeo sobre el mismo modelo:

Ahora, quiero mapear CADA tabla a una tabla:

 

La solución tiene dos proyectos, uno del proyecto de consola, y el otro es una librería de clase:

Pueden bajar el código desde NHibernate3ItemsTablePerClass.zip. Necesitan agregar las librerías NHibernate (ver post anterior para más detalles). La base de datos puede crearse ejecutando ExecuteAll.cmd en el directorio Sql. El comando acepta un parámetro (como ExecuteAll (local)) que indique el servidor de base de datos. El valor asumido es .\SQLEXPRESS.

El mapeo de Item:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerClass.Domain"
namespace="ItemsTablePerClass.Domain">
  <class name="Item" table="Items">
    <id name="Id">
      <generator class="guid.comb" />
    </id>
    <property name="Title" not-null="true" />
    <property name="Description" not-null="true" />
  </class>
</hibernate-mapping>

Hay un atributo table, y ya no hay valor de discrimiator. Lo nuevo está en el mapeo de Note:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerClass.Domain"
namespace="ItemsTablePerClass.Domain">
  <joined-subclass name="Note" extends="Item" table="Notes">
    <key column="Id" />
    <property name="Content" />
  </joined-subclass>
</hibernate-mapping>

El nuevo elemento es joined-subclass. Hay un atributo de table para este mapeo, también. Y no hay elemento id. Fue reemplazado por el elemento  key que uno ambas tablas.

Cambios similares en el mapeo de Page:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerClass.Domain"
namespace="ItemsTablePerClass.Domain">
  <joined-subclass name="Page" extends="Item" table="Pages">
    <key column="Id" />
    <property name="Url"/>
  </joined-subclass>
</hibernate-mapping>

Notablemente, el código principa es el mismo que en los anteriores ejemplos. Y las clases del dominio siguen sin cambios.

Los próximos pasos: comenzar a investigar mapeos uno-a-varios, otras alternativas de mapeo (como Fluent NHibernate, ConfORM, y la nueva API de NHibernate 3.2).

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

NHibernate 3 (Parte 3) Tabla por Clase Concreta

Monday, April 18th, 2011

Anterior Post
Siguiente Post

Quiero usar el mismo dominio que en mi anterior post:

 

but this time, I want two tables: one for Notes, another for Pages:

La solución tiene dos proyectos: una librería de clases conteniendo el dominio, y un proyecto de consola:

El código está en mi AjCodeKatas Google code project, en el directorio trunk/NHibernate/ItemsTablePerConcreteClass. El código no incluye las librerías de NHibernate, hay que agregarlas a las referencias de la solución. Como en el ejemplo previo, agregué como referencias al proyecto de consola: NHibernate.dll del directorio de NHibernate llamado Required_Bins y NHibernate.ByteCode.Castle del directorio Required_For_LazyLoading\Castle. Agregué también los archivos .xsd  (los esquemas XML para soportar el manejo de intellisense cuando se escriben los archivos de mapeo) desde el directorio de NHibernate llamado Required_Bins.

Pueden bajar el código fuente directamente desde NHibernate3ItemsTablePerConcreteClass.zip, si no quieren bajarlo con un cliente de SVN desde el proyecto de Google Code. Si necesitan las librerías de NHibernate 3.x, puede visitar http://nhforge.org o bajar las que utilicé en mi primer ejemplo NHibernate3SimpleMapping.zip, ahí están incluidas. Pueden ver mi primer post para otros detalles (por ejemplo, no olviden poner los archivos de mapeao como recursos embebidos, si van a construir su propia solución).

Noten que las clases del dominio siguen siendo las mismas: no cambiaron desde el anterior ejemplo. Lo que cambia es la forma de mapeo, los archivos de mapeo, no el código. Esta es una prueba de la independencia del dominio de nuestra estrategia de mapeo.

Este es el Item.hbm.xml:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerConcreteClass.Domain"
namespace="ItemsTablePerConcreteClass.Domain">
  <class name="Item" abstract="true">
    <id name="Id">
      <generator class="guid.comb" />
    </id>
    <property name="Title" not-null="true" />
    <property name="Description" not-null="true" />
  </class>
</hibernate-mapping>

 

 

Primero removí la columna discriminator. No hay tal columna, porque no hay ahora un tabla de Items. Agregué el atributp table. I added the attribute abstract=”true”. Si olvidan agregarlo, NHibernate “piensa” que tienen una tabla concreta para guardar los objetos de esta clase. Pueden removerla, si tiene Items que no son Notas ni Páginas. No es el caso de este ejemplo que quiero implementar: los únicos items “reales” son Notas o Páginas. Item es una abstracción del dominio.

El Note.hbm.xml:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerConcreteClass.Domain"
namespace="ItemsTablePerConcreteClass.Domain">
  <union-subclass name="Note" extends="Item" table="Notes">
    <property name="Content" />
  </union-subclass>
</hibernate-mapping>

 

 

Agregué el atributo table=”Notes”. Y el nuevo elemento que se usa es union-subclass, en vez de subclass como en el anterior ejemplo. Este nuevo elemento “dice”: “la tabla de esta clase CONTIENE las columnas a grabar con los valores de la clase padre”. Esto es: las columnas de la tabla son la unión de las propiedades de la clase padre y de esta clase.

Cambios similares en Page.hbm.xml:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerConcreteClass.Domain"
namespace="ItemsTablePerConcreteClass.Domain">
  <union-subclass name="Page" extends="Item" table="Pages">
    <property name="Url"/>
  </union-subclass>
</hibernate-mapping>

 

 

El código principal es prácticamente el mismo que en el anterior ejemplo:

ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
using (ISession session = sessionFactory.OpenSession())
{
    using (ITransaction tx = session.BeginTransaction())
    {
        Page page1 = new Page();
        page1.Title = "Technical Blob";
        page1.Description = "My Personal Technical Blob in English";
        page1.Url = "http://ajlopez.wordpress.com";
        Page page2 = new Page();
        page2.Title = "My Personal Blob";
        page2.Description = "My Personal Blob in Spanish";
        page2.Url = "http://ajlopez.zoomblog.com";
        Note note1 = new Note();
        note1.Title = "To Do";
        note1.Description = "My To Do List";
        note1.Content = "Practice NHibernate";
        session.Save(page1);
        session.Save(page2);
        session.Save(note1);
        var items = session.Query<Item>(); ;
        foreach (Item item in items)
            System.Console.WriteLine(string.Format("Item {0}", item.Title));
        tx.Commit();
        session.Close();
    }
}

 

 

excepto que esta vez, comencé a usar Session.Query<T>, un nuevo método de extensión para las sesiones de NHibernate, que retorna un IQueryable<T> que soporta Linq contra NHibernate y la base de datos de persistencia. Para usar ese método HAY QUE AGREGAR:

using NHibernate.Linq;

sino, no aparecerá como disponible. Recuerden: es un método de extensión, no es un método “natural” de ISession.

Mis fuentes de información:

El libro: Jason Dentler’s NHibernate 3.0 Cookbook

http://www.nhforge.org/doc/nh/en/index.html#mapping-declaration-unionsubclass

http://www.nhforge.org/doc/nh/en/index.html#inheritance

Próximos pasos: implementar Tabla por Clase, explorar relaciones uno-a-varios, colecciones “lazy”, etc…

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

NHibernate 3 (Part 2) Tabla por Jerarquía

Saturday, April 9th, 2011

Anterior post
Siguiente post

En mi anterior post de esta serie, implementé un mapeo simple: una clase, una tabla.

Ahora, tengo tres clases, usando herencia:

El Id es un System.Guid. Quiero diseñar una colección de items. Cada item puede ser una Nota (Note), con contenido de texto, o una página Web (Page) con una URL asociada. La solución que preparé tiene dos proyectos:

Estoy manteniendo este código en mi AjCodeKatas Google code project, en el directorio trunk/NHibernate/ItemsTablePerHierarchyMapping. El código no incluye las librerías de NHibernate, así que tuve que agregar referencias a la solución. Agregué como referencias al proyecto de consola: NHibernate.dll desde el directorio de NHibernate llamado Required_Bins, y la dll  NHibernate.ByteCode.Castle del directorio de NHibernate llamado Required_For_LazyLoading\Castle.

Si son perezosos como yo, y quieren tener la solución ahora, pueden bajar mi anterior ejemplo que contiene las librerías de NHibernate que necesitamos para el presentet: NHibernate3SimpleMapping.zip. El código de este post está en: NHibernate3ItemsTablePerHierarchy.zip.

El proyecto Domain no tiene referencia a las librerías de NHibernate. TOdo el mapeo está en el proyecto de consola. Los archivos .hbm está ahí agregados como recursos embebidos (no se olviden de agregarlos así en sus proyectos; de la forma que planteo la configuración de NHibernate (ver más abajo) esta librería busca archivos hbm en el assembly que le indiquemos; no es la única forma de indicarle los mapeos: espero en próximos posts explorar otras alternativas).

Tengo 3 clases para mapear. Pero quiero almacenar todos los datos de cualquier item en una sola tabla:

Pueden generar la base de datos vacía ejecutando el comando ExecuteAll.cmd que he dejado en el código de ejemplo.

La tabla Items tiene una columna ItemType, que indica si un item es una Note o una Page. El archivo de mapeo Item.hbm.xml referencia esa columna como un discriminador:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerHierarchyMapping.Domain"
namespace="ItemsTablePerHierarchyMapping.Domain">
  <class name="Item" table="Items">
    <id name="Id">
      <generator class="guid.comb" />
    </id>
    <discriminator column="ItemType" />
    <property name="Title" not-null="true" />
    <property name="Description" not-null="true" />
  </class>
</hibernate-mapping>

Noten que no tengo esa columna reflejada en una propiedad de las clases: el discriminador lo usa el NHibernate, no lo necesitamos nosotros en nuestro dominio.

Bueno, ahora necesito mapear los objetos Note. ¿Cómo escribir ese mapeo? Vean el Note.hbm.xml:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerHierarchyMapping.Domain"
namespace="ItemsTablePerHierarchyMapping.Domain">
  <subclass name="Note" extends="Item" discriminator-value="Note">
    <property name="Content" />
  </subclass>
</hibernate-mapping>

El nuevo elemento XML que encontramos es subclass. Tiene un atributo extends que referencia a la clase padre. El atributo discriminator-value tiene el valor a poner en la columna discriminadora cuando el Item es una Note. Si no especificamos el valor de discriminador, el nombre de la clase sería usada como valor asumido, en este caso ItemsTablePerHierarchyMappin.Domain.Note.

El mapeo de Page.hbm.xml es similar:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="ItemsTablePerHierarchyMapping.Domain"
namespace="ItemsTablePerHierarchyMapping.Domain">
  <subclass name="Page" extends="Item" discriminator-value="Page">
    <property name="Url"/>
  </subclass>
</hibernate-mapping>

Podemos ejecutar este código de consola:

ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
using (ISession session = sessionFactory.OpenSession())
{
    using (ITransaction tx = session.BeginTransaction())
    {
        Page page1 = new Page();
        page1.Title = "Technical Blob";
        page1.Description = "My Personal Technical Blob in English";
        page1.Url = "http://ajlopez.wordpress.com";
        Page page2 = new Page();
        page2.Title = "My Personal Blob";
        page2.Description = "My Personal Blob in Spanish";
        page2.Url = "http://ajlopez.zoomblog.com";
        Note note1 = new Note();
        note1.Title = "To Do";
        note1.Description = "My To Do List";
        note1.Content = "Practice NHibernate";
        session.Save(page1);
        session.Save(page2);
        session.Save(note1);
        IQuery query = session.CreateQuery("from Item");
        foreach (Item item in query.List<Item>())
			System.Console.WriteLine(string.Format("Item {0}", item.Title));
        tx.Commit();
        session.Close();
    }
}
System.Console.ReadKey();

para insertar y listar items.

Esta estrategia de mapeo (el viejo truco de grabar cada subclase en una misma table, usando discriminadores 😉 es llamada Table Per Hierarchy (Fowler la menciona como Single Table Inheritance). Acá la traduje como Tabla por Jerarquía.

 

Próximos pasos: implementar otras estrategias de mapeo: una tabla por clase concreta (en este caso, Pages y Notes); una tabla por clase  (sería Items, Pages, Notes).

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

NHibernate 3 (Parte 1) Un mapeo simple

Tuesday, January 18th, 2011

Siguiente post

Hace año y medio escribí un ejemplo en NHibernate 2.0:

First NHibernate 2.x Example

Este año que recién comienza, quiero explorar el nuevo release NHibernate 3.0 con ejemplos. Pueden bajar la versión actual de este ORM para .NET desde:

http://sourceforge.net/projects/nhibernate/files/

Uso NHibernate-3.0.0.GA-bin.zip para la solución:

Mantengo este ejempo en mi proyecto AjCodeKatas, en el directorio trunk/NHibernate/SimpleMapping. El código no incluye las librerías de NHibernate, así que tienen que agregar las referencias a los proyectos. Yo agregué al proyecto SimpleMapping.Console todas las .dll de los directorios de NHibernate NHibernate Required_Bins y Required_For_LazyLoading\Castle.

Si son perezosos, como yo ;-), pueden directamente bajarse el ejemplo completo con las librerías incluidas desde mi Skydrive: NHibernate3SimpleMapping.zip.

Noten que creé una solución con un Solution folder Schemas, en el que agregué los archivos de esquemas .xsd que encontré en el directorio Required_Bins dentro de la distribución binaria de NHibernate. Esto permite que puedan usar Intellisense cuando escriban algunos archivos que NHibernate necesita.

El proyecto SimpleMapping.Domain NO referencia a NHibernate (una prueba de la independencia de nuestro dominio de la persistencia) y contiene una sola clase:

    public class Customer
    {
        public virtual Guid Id { get; set; }
        public virtual string Name { get; set; }
        public virtual string Address { get; set; }
        public virtual string Notes { get; set; }
    }

Escribí un app.config en la aplicación de consola:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section
    name="hibernate-configuration"
    type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"
/>
  </configSections>
  <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
      <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="connection.connection_string">Data Source=.\SQLEXPRESS;Initial Catalog=NHibernate3SimpleMapping;Integrated Security=True</property>
      <property name="proxyfactory.factory_class">
        NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
      </property>
      <mapping assembly="SimpleMapping.Console" />
    </session-factory>
  </hibernate-configuration>
</configuration>

Tenemos que ir estudiando los parámetros que necesita NHibernate. Pero a simple vista: aparece el string de conexión, y el dialecto. Vamos a tener que estudiar qué es esos del dialecto: un adelanto rápido, es la clase de NHibernate que sabe cómo generar comandos SQL para la base de datos que estemos usando. NHibernate puede acceder a otros tipos de bases de datos, no sólo a SQL Server, y el dialecto es la forma que tiene de poder generar comandos SQL especiales para cada base de datos (por ejemplo, el manejo de recuperación autonuméricos).

Escribí un archivo Customer.hbm.xml. Atención: Lo marqué como Embedded Resources en su ventana de propiedades. No se olviden de este paso: en este ejemplo, le estamos diciendo en el parámetro mapping de la configuración de más arriba, que todos los mapeos están incluidos en el assembly SimpleMapping.Console. Vean que las clases pueden estar definidas en un proyecto, y los mapeos en otro. Este es el contenido del Customer.hbm.xml:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="SimpleMapping.Domain"
namespace="SimpleMapping.Domain">
  <class name="Customer" table="Customers">
    <id name="Id">
      <generator class="guid.comb" />
    </id>
    <property name="Name" not-null="true" />
    <property name="Address" />
    <property name="Notes" />
  </class>
</hibernate-mapping>

Para crear la base de datos, pueden ejecutar Sql\ExecuteAll.cmd (ver Readme.txt para más detalles).

El programa de consola solamente crea un cliente, lo graba en la base, y recupera todos los clientes existentes:

    ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
    using (ISession session = sessionFactory.OpenSession())
    {
        using (ITransaction tx = session.BeginTransaction())
        {
            Customer customer1 = new Customer();
            customer1.Name = "Customer 1";
            customer1.Address = "Address 1";
            customer1.Notes = "Notes 1";
            session.Save(customer1);
            IQuery query = session.CreateQuery("from Customer");
    
            foreach (Customer c in query.List<Customer>())
                System.Console.WriteLine(string.Format("Customer {0}", c.Name));
            tx.Commit();
            session.Close();
        }
    }

(Disculpen, no estoy muy creativo para los los valores de las propiedades 😉

Hay varios detalles para discutir: ¿Qué es eso del Castle bytecode? ¿Para qué sirve cada propiedad que pusimos en la configuración? ¿Qué es una session? ¿Y una transacción? ¿Qué es un Id? ¿Qué es un guid.comb? ¿Qué alternativas tenemos a haber puesto ese valor? ¿Cómo mapear una propiedad de otro tipo, como Currency, DateTime, etc..?.

¿Y qué pasa si necesitamos más clases? ¿Y el manejo de la herencia? ¿Y cómo tratamos las relaciones uno a varios? Y muchos otros puntos, todo a explorar en los siguientes post de la serie.

Mientras, algunos recursos:

Online NHibernate Documentation
NHibernate available resources
SourceForge project

Libro: Jason Dentler’s NHibernate 3.0 Cookbook

http://delicious.com/ajlopez/nhibernate+tutorial

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

Generando una solución con AjGenesis usando archivos hbm de NHibernate

Monday, July 5th, 2010

Hace unos meses, escribí un ejemplo de generación de código que usa los archivos .hbm como modelo inicial:

Generating Code with AjGenesis Using NHibernate Hbm Files

Generando código con AjGenesis usando archivos de mapeo de NHibernate

Ya conocen que AjGenesis puede usar como modelo lo que uno decida. Prefiero un modelo libre, pero, según el bueno de Fabio Maulo, “los archivos hbm los usan un montón de gente”. Estos ejemplos me sirven también para demostrar que se puede usar un modelo distinto de los que propongo en otros ejemplos.

Desde esos posts, he mejorado el ejemplo. Pueden bajarlo del trunk del proyecto en Codeplex:

i

Como lo mío es un apostolado, les dejo preparado un paquete listo para ejecutar, en mi Skydrive, que incluye el ejemplo y los binarios de AjGenesis en desarrollo:

Examples > AjGenesis > NHibernateMappingExample02.zip

Esta es su estructura de directorios:

Hay dos proyectos: AjFirstExample, y AjTest. El primero tiene solamente dos simples entidades. El segundo tiene más entidades, con relaciones uno a varios. Pueden generar el código para una solución completa, lanzando los comandos:

GenerateAjFirstExample.cmd
GenerateAjTest.cmd

Son simples “shortcuts” para los comandos

GenerateProject AjFirstExample
GenerateProject AjTest

Como ejemplo, esta es la solución que genera para el proyecto AjTest (ver que queda dentro de un directorio Build, que si no existe lo crea):

Pueden ejecutar el sitio web (el script para generar la base de datos, está dentro del directorio Sql de cada proyecto, ej Projects\AjTest\Sql):

En este ejemplo, uso los archivos .hbm como modelo. Lo nuevo del ejemplo, es que enriquecí el modelo con el uso de tags meta, como en este  Employee.hbm de AjTest:

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
  assembly="AjTest.Entities"
  namespace="AjTest.Entities"
  >
  <class name="Employee" table="employees">
    <meta attribute="SetName">Employees</meta>
    <meta attribute="Descriptor">Employee</meta>
    <meta attribute="SetDescriptor">Employees</meta>
    
    <id name="Id" column="Id" type="Int32">
      <meta attribute="Description">Id</meta>
      <generator class="native"/>
    </id>
    <property name="EmployeeCode" type="String">
      <meta attribute="Description">Employee Code</meta>
    </property>
    <property name="LastName" type="String">
      <meta attribute="Description">Last Name</meta>
    </property>
    <property name="FirstName" type="String">
      <meta attribute="Description">First Name</meta>
    </property>
    <many-to-one name="Department" column="IdDepartment"
                 class="AjTest.Entities.Department, AjTest.Entities" />
    <bag name="Tasks" lazy="true" inverse="true" cascade="all">
      <key column="IdEmployee"/>
      <one-to-many class="AjTest.Entities.Task, AjTest.Entities"/>
    </bag>  
    <bag name="EmployeeSkills" lazy="true" inverse="true" cascade="all">
      <key column="IdEmployee"/>
      <one-to-many class="AjTest.Entities.EmployeeSkill, AjTest.Entities"/>
    </bag>  
  </class>
</hibernate-mapping>

Note the use of the meta tag at class level, and at property level. AjGenesis run tasks, one is Tasks\LoadMappings.ajg:

include "Utilities/Utilities.tpl"
include "Utilities/FileUtilities.tpl"
include "Utilities/TypeUtilities.tpl"
Include("Utilities/NHibernateUtilities.tpl")
include "Templates/CSharp/UtilitiesCs.tpl"
include "Templates/CSharp/CSharpFunctions.tpl"
AssemblyManager.LoadFrom("Libraries/NHibernate.dll")
parser = new NHibernate.Cfg.MappingSchema.MappingDocumentParser()
if not Project.BuildDir then
  Project.BuildDir = "Build/${Project.Name}/CSharp"
end if
Project.Entities = CreateList()
for each MappingName in Project.Mappings
  filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml"
  mapping = parser.Parse(OpenAsStream(filename))
    
  for each hbmclass in mapping.Items where IsType(hbmclass, "HbmClass")
    Entity = CreateObject()
    
    Project.Entities.Add(Entity)
  
    Entity.ClassName = hbmclass.name
    Entity.Name = hbmclass.name
    Entity.Namespace = mapping.namespace
    
    Message "Processing Mapping of Entity " & Entity.Name
        
    Entity.Properties = CreateList()
    
    if hbmclass.Id then
      Property = CreateObject()
      Property.Name = hbmclass.Id.name
      Property.Type = HbmTypeToCSharp(hbmclass.Id.type1, Entity.Namespace)
      Property.IsId = True
      for each meta in hbmclass.Id.meta
        Property.SetValue(meta.attribute, meta.GetText())
      end for
      Entity.Properties.Add(Property)
    end if
    
    for each meta in hbmclass.meta
      Entity.SetValue(meta.attribute, meta.GetText())
    end for
        
    for each item in hbmclass.Items
      Message "Processing Item " & item.GetType().Name
      
      if IsType(item, "HbmProperty") then
        Property = CreateObject()
        Property.Name = item.name
        Property.SetValue("Description", "Description " & item.name)
        Property.Type = HbmTypeToCSharp(item.type1, Entity.Namespace)
        Entity.Properties.Add(Property)
        for each meta in item.meta
          Property.SetValue(meta.attribute, meta.GetText())
        end for
      end if
      
      if IsType(item, "HbmManyToOne") then
        Property = CreateObject()
        Property.Name = item.name
        Property.Type = HbmTypeToCSharp(item.class, Entity.Namespace)
        Property.Reference = HbmTypeToCSharp(item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmSet") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsSet = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmBag") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsList = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
    end for    
  end for
end for

Noten el uso de Property.SetValue(…), Entity.SetValue(…) para enriquecer el modelo en-memoria con la nueva metadata contenida en los archivos de mapeo. Cualquier dato simple que en anteriores ejemplos ponía en los modelos libres, ahora puede estar en el hbm. Como en otros ejemplos de AjGenesis, uso la metadata para mostrar leyendas, textos en la presentación, o para tomar decisiones o usarlas como quiera en las tareas y templates.

Este es un ejemplo “proof-of-concept”. Debería ser mejorado para usar todo el poder de NHibernate. Pero es una demostración de un modelo inicial, para crear gran cantidad de artefactos de textos, en este caso, una aplicación lista para ejecutar y extender.

Tengo que escribir más ejemplos, volviendo a usar un modelo libre, como adelanté en:

AjGenetizando una aplicación

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

ALT.NET Café sobre ConfORM de Fabio Maulo

Friday, February 26th, 2010

La gente de la comunidad ALT.NET Hispano organiza para mañana sábado una reunión virtual, no conferencia, sino ALT.NET Café. La idea es presentar y probar el nuevo trabajo del bueno de @FabioMaulo, su proyecto ConfORM. Este es el anuncio:

El sábado 27 de febrero tendremos una reunión virtual. Será un Alt.Net Cafe virtual con el tema "ConfORM", con Fabio Maulo Fecha: Sábado 27 de Febrero a las 18:00 hrs. Hora Internacional (GMT/UTC), con una duración aproximada de 2 horas. Para atender la reunión deben usar el enlace: http://snipr.com/virtualaltnet el cual les demandará el uso del programa Microsoft Office Live Meeting 2007 Client. Para mas información sobre ConfORM vean el blog de Fabio http://is.gd/96pXs. Encontrará más información sobre lo necesario para atender la reunión en Descripcion de Reuniones VAN.

Qué es ConfORM? Pueden leer los post de Fabio en su blog:

Map NHibernate using your API

ConfORM: NHibernate un-Mapping

Toma un modelo de dominio YA compilado en una .dll, lo examina por reflection, como el del post:

y mediante agregados por código se construye el mapeo de NHibernate, sin usar archivos de configuración de XML. Ahora, Fabio está pidiendo modelos de ejemplo para probarlo, es parte de la idea de la reunión de mañana.

No entendí del todo la intención de este proyecto, enmarcado en una idea más grande que Fabio llama Orum (me temo que Fabio mencionó la idea en el post, pero no encontré en enlace donde la explica, la recuerdo de las discusiones de la lista). En particular, no conozco como generar XMLs, dice Fabio:

ConfOrm does not generate XML mappings but you can easy create XMLs to check how ConfOrm work.

Pueden bajar el código para probarlo desde:

http://code.google.com/p/codeconform/

Pueden leer la discusión en la lista de ALT.NET Hispano en:

Alt.Net Café sobre ConfORM

Ahí pueden leer como enviarle modelos de prueba a Fabio.

Hoy viernes, Fabio escribió:

mañana vamos a probar ConfORM… aún estan a tiempo de enviar algún domain (entidades).

No se necesitan los fuentes, con la DLL alcanza ustedes, pero, tengan los fuentes a mano y estas tres cosas:

  • which is the strategy to represent a class hierarchy
  • which is the relation between classes
  • how manage cascade-actions (read it as: which are aggregate-root)

Pueden ver una discusión y ejemplo de su uso en:

Demo.cs – rgb-playground – Project Hosting on Google Code (acá parece que está el código para generar XML y ver el resultado del mapeo)

Modeling Files and Folders ala FileSystem – nhusers | Google Groups

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

Generando código con AjGenesis usando archivos de mapeo de NHibernate

Monday, November 23rd, 2009

En estos días, estuve trabajando en generar código de clases C#, usando como punto de partida los archivos .hbm, que se usan en NHibernate para especificar el mapeo de clases y tablas de bases relacionales. Como es usual, cuando encaro algo de generación de código uso AjGenesis, mi proyecto open source de generación de código (practico el “dog fooding” :-).

(English version of this post Generating code with AjGenesis using NHibernate hbm files)

Pueden bajar un ejemplo de lo que estoy haciendo de mi Skydrive:

Examples > AjGenesis > NHibernateMappingExample01.zip

(el código en desarrollo está en el trunk, en este change actual, en el directorio examples\NHibernateMappinp:

pero si quieren ir directamente al ejemplo, pueden bajárselo completo desde el Skydrive que mencioné, que incluye el ejecutable de AjGenesis de la versión en desarrollo, no necesitan compilar nada).

Luego de expandir el archivo del ejemplo, tendrán este contenido:

 

Para crear clases C#, pueden probar de ejecutar los comandos:

GenerateClasses AjFirstExample
GenerateClasses AjTest

Para crear un proyecto .NET con los archivos .cs y .hbm, y una solución, ejecutar:

GenerateProject AjFirstExample
GenerateProject AjTest

Los archivos generados, en ambos casos, quedan en el directorio Build.

Hay dos proyectos ejemplo que son AjFirstExample, con dos mapeos simples, y AjTest, que tiene mapeos más interesantes, con “bags” y relaciones “many to one”.

En el ejemplo, cada proyecto se describe con un simple archivo Project.xml:

<Project Name="AjTest">
</Project>

En cuanto necesite más información, lo agregaré ahí, o en tags meta de los propios archivos de mapeo y configuración.

Este es uno de los archivos de mapeo que sirven de modelo inicial para este ejemplo de generación, en Projects\AjTest\Mappings, Department.hbm:

<?xml version="1.0" encoding="utf-8" ?> 
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
  assembly="AjTest.Entities"
  namespace="AjTest.Entities"
  >
  <class name="Department" table="departments">
    <id name="Id" column="Id" type="Int32">
      <generator class="native"/>
    </id>
    <property name="Description" type="String"/>
    <bag name="Employees" lazy="true" inverse="true" cascade="all">
      <key column="IdDepartment"/>
      <one-to-many class="AjTest.Entities.Employee, AjTest.Entities"/>
    </bag>  
  </class>
</hibernate-mapping>

Este es el código generado para este mapeo, Department.generated.cs:

using System;
using System.Collections.Generic;
using Iesi.Collections.Generic;
namespace AjTest.Entities 
{
  public class Department {
    public int Id { get; set; }
    public string Description { get; set; }
    public IList<Employee> Employees { get; set; }
    public Department() 
    {
      this.Employees = new List<Employee>();
    }
  }
}

Veamos el proceso de generación. Este es el contenido de GenerateProject.cmd:

@echo off
set ProjectName=%1%
if "%1%"=="" set ProjectName=AjFirstExample
Bin\AjGenesis.Console.exe Projects\%ProjectName%\Project.xml Tasks\AddMappings.ajg Tasks\BuildCSharp.ajg
xcopy Libraries\*.* Build\%ProjectName%\CSharp\Src\Libraries /Y /Q

La línea más importante es la que invoca a AjGenesis.Console.exe. El contenido de Project.xml se carga en memoria. La tarea AddMapping.ajg se carga y ejecuta (está escrita en un lenguaje dinámico, afectuosamente llamado AjBasic), y luego, se procesa la tarea BuildCSharp.ajg. Veamos el código de AddMapping.ajg:

' Add mappings from directory if not specified in Project model
Include("Utilities/Utilities.tpl")
if not Project.Mappings then
  Project.Mappings = CreateList()
  
  di = new System.IO.DirectoryInfo("Projects/${Project.Name}/Mappings")
  
  for each fi in di.GetFiles("*.hbm.xml")
    filename = fi.Name
    Project.Mappings.Add(filename.Substring(0, filename.Length - 8))
  end for
end if

Encuentra y agrega los nombres de los archivos de mapeo contenidos en el directorio de Mapping, dentro del proyecto (noten que se pueden usar clases y objetos del framework .NET). Una tarea más interesante es la GenerateCSharp.ajg. Primero, carga la dll de NHibernate, para usar más adelante su parser de archivos hbm:

include "Utilities/Utilities.tpl"
include "Utilities/FileUtilities.tpl"
include "Utilities/TypeUtilities.tpl"
Include("Utilities/NHibernateUtilities.tpl")
include "Templates/CSharp/UtilitiesCs.tpl"
include "Templates/CSharp/CSharpFunctions.tpl"
AssemblyManager.LoadFrom("Libraries/NHibernate.dll")
parser = new NHibernate.Cfg.MappingSchema.MappingDocumentParser()

Crea objetos dinámicos, donde coloca información de la solución y el proyecto a crear:

if not Project.BuildDir then
  Project.BuildDir = "Build/${Project.Name}/CSharp"
end if
message "Creating Directories..."
FileManager.CreateDirectory(Project.BuildDir)
FileManager.CreateDirectory("${Project.BuildDir}/Sql")
FileManager.CreateDirectory("${Project.BuildDir}/Src")
FileManager.CreateDirectory("${Project.BuildDir}/Src/Libraries")
message "Defining Solution and Projects..."
Project.Solution = CreateObject()
Project.Solution.Guid = "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"
Project.Solution.Projects = CreateList()
message "Defining Entities Project..."
PrjEntities = CreateObject()
PrjEntities.Includes = CreateList()
PrjEntities.Guid = CreateGuid()
PrjEntities.COMGuid = CreateGuid()
Project.Solution.Projects.Add(PrjEntities)
Project.Entities = CreateList()

Ahora, itera sobre cada archivo hbm, lo lee usando el parser del propio NHibernate, toma información sobre las clases a generar:

for each MappingName in Project.Mappings
  filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml"
  mapping = parser.Parse(OpenAsStream(filename))
    
  for each hbmclass in mapping.Items where IsType(hbmclass, "HbmClass")
    Entity = CreateObject()
    
    Project.Entities.Add(Entity)
  
    Entity.ClassName = hbmclass.name
    Entity.Namespace = mapping.namespace
    
    ' Namespace as default project name for Entities Project
    if not PrjEntities.Name then
      PrjEntities.Name = mapping.namespace
      PrjEntities.Directory = "${Project.BuildDir}/Src/${PrjEntities.Name}"
      FileManager.CreateDirectory(PrjEntities.Directory)
    end if
    
    Entity.Properties = CreateList()
    
    if hbmclass.Id then
      Property = CreateObject()
      Property.Name = hbmclass.Id.name
      Property.Type = HbmTypeToCSharp(hbmclass.Id.type1, Entity.Namespace)
      Entity.Properties.Add(Property)
    end if
    
    for each item in hbmclass.Items
      if IsType(item, "HbmProperty") then
        Property = CreateObject()
        Property.Name = item.name
        Property.Type = HbmTypeToCSharp(item.type1, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      
      if IsType(item, "HbmManyToOne") then
        Property = CreateObject()
        Property.Name = item.name
        Property.Type = HbmTypeToCSharp(item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmSet") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsSet = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
      if IsType(item, "HbmBag") then
        Property = CreateObject()
        Property.Name = item.name
        Property.IsList = true
        Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace)
        Entity.Properties.Add(Property)
      end if
    end for    
  end for
end for

Pueden extender esta capacidades, procesando más tags (debería escribir un ejemplo usando los tags meta que puede contener el hbm; ya utilitarios de Java, como el venerable XDocLet, usaban esos tags para ayudarse en la generación de código, en Hibernate), y detectar más formas de mapeo de NHibernate. Ahora, pasa a la generación de código:

for each Entity in Project.Entities
  TransformerManager.Transform("Templates/CSharp/Entity.tpl", "${PrjEntities.Directory}/${Entity.ClassName}.generated.cs", Environment)
  PrjEntities.Includes.Add(CreateFileCs("${Entity.ClassName}.generated"))
end for

La tarea genera los archivos .cs, y también crea un archivo de solución y otro de proyecto C#, copiando y embebiendo los archivos de mapeo originales:

for each MappingName in Project.Mappings
  filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml"
  targetfilename = "${PrjEntities.Directory}/${MappingName}.hbm.xml"
  System.IO.File.Copy(filename, targetfilename, true)
  PrjEntities.Includes.Add(CreateFileType(MappingName,"hbm.xml"))
end for
for each CsProject in Project.Solution.Projects where CsProject.ProjectType<>"Web"
  FileManager.CreateDirectory(CsProject.Directory)
  FileManager.CreateDirectory(CsProject.Directory & "/Properties")
  TransformerManager.Transform("Templates/CSharp/CsProject.tpl", "${CsProject.Directory}/${CsProject.Name}.csproj", Environment)
  TransformerManager.Transform("Templates/CSharp/AssemblyInfoCs.tpl", "${CsProject.Directory}/Properties/AssemblyInfo.cs", Environment)
end for
TransformerManager.Transform("Templates/Solution.tpl", "${Project.BuildDir}/Src/${Project.Name}.sln", Environment)

Esta es la solución generada:

Próximos pasos

Debería trabajar en algunos puntos:

– Generar una solución más completa, como en otros ejemplos de AjGenesis (con un proyecto de infraestructura, un proyecto de Service Layer o similar, una presentación web, etc…).

– Soportar más opciones de mapeo de NHibernate.

– Usar los tags meta.

Por ahora, pueden jugar con este ejemplo. Pueden cambiar los templates para generar más artefactos, por ejemplo, código VB.NET.

Gracias a @fabiomaulo por avisarmr de las capacidades públicas de parser de hbm dentro de NHibernate!

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez