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

Published on Author lopezLeave a comment

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

Leave a Reply

Your email address will not be published. Required fields are marked *