Posts tagged linq
Carga temprana de colecciones (eager loading) con nHibernate
20 Dec 10
Unos de los aspectos que más me ha costado encontrar documentación ha sido sobre cómo realizar la carga temprana de colecciones hijas de una entidad. Hay escenarios en el que la carga vaga de entidades no es viable, ya sea porque la arquitectura física de tu aplicación no te lo permite, porque te ha emergido el típico problema de “SELECT N+1” o por cualquier otra razón de tu proyecto.
Partamos del siguiente escenario. Este escenario es una versión simplificada de la base de datos de pruebas AdventureWorks de Microsoft:

Vamos a ir viendo diferentes métodos para realizar la precarga de las entidades hijas utilizando MultiCriteria, Future<> y LINQ.
Si quieres usar HQL para realizar la precarga, puedes encontrar un ejemplo al respecto en el blog “The nHibernate FAQ“.
Hagamos una carga del producto con ID 707 y carguemos sus SalesOrdelDetails y su ProductModel usando ICriteria:
[Fact]
public void CargaTempranaConModeloYDetalleMetodo1()
{
using (ISession session = _sessionFactory.OpenSession())
{
ICriteria criteria = session.CreateCriteria<Product>();
Product product = criteria.Add(Restrictions.Eq("ProductId", 707))
.SetFetchMode("ProductModel", FetchMode.Eager)
.SetFetchMode("SalesOrderDetail", FetchMode.Eager)
.UniqueResult<Product>();
Assert.True(NHibernateUtil.IsInitialized(product.ProductModel));
Assert.True(NHibernateUtil.IsInitialized(product.SalesOrderDetail));
}
}
SetFechMode me permite carga de forma temprana la colección de elementos hijos de la propiedad definida. Lo que genera este método es un “LEFT OUTER JOIN” en la consulta SQL. La consulta generada es la siguiente:
SELECT ...
FROM SalesLT.[Product] this_
left outer join SalesOrderDetail salesorder2_ on this_.ProductId = salesorder2_.ProductId
left outer join [ProductModel] productmod3_ on this_.ProductModelID = productmod3_.ProductModelId
WHERE this_.ProductId = @p0;@p0 = 707
Puedes observar los dos “LEFT OUTER JOIN” generados, uno para el modelo y otro para el detalle de la venta.
Pero cuidado, si vamos a cargar más de un nivel de nuestra entidad como es en este ejemplo, el realizar la consulta con varios “LEFT OUTER JOIN” en la misma consulta nos puede generar una cantidad ingente de resultados en todas sus combinaciones por el producto cartesiano generado al unir las tablas. En este ejemplo sencillo no notaríamos merma de rendimiento ya que es muy simple, pero en escenarios más complejos puede ser un problema muy grave.
Carga temprana mediante un MultiCriteria
Para estos casos, nHibernate no da otros mecanismos para realizar la consulta. Uno de ellos es el MultiCriteria. En una misma consulta a la base de datos, se ejecutar dos SELECT, una para cada nivel. Veamos el código:
[Fact]
public void CargaTempranaConModeloYDetalleMetodo2()
{
using (ISession session = _sessionFactory.OpenSession())
{
DetachedCriteria criteria1 =
DetachedCriteria.For<Product>()
.Add(Restrictions.Eq("ProductId", 707))
.SetFetchMode("ProductModel", FetchMode.Eager);
DetachedCriteria criteria2 =
DetachedCriteria.For<Product>()
.Add(Restrictions.Eq("ProductId", 707))
.SetFetchMode("SalesOrderDetail", FetchMode.Eager);
IList result = session.CreateMultiCriteria()
.Add(criteria1)
.Add(criteria2)
.List();
Product product = (Product)((IList)result[0])[0];
Assert.True(NHibernateUtil.IsInitialized(product.ProductCategory));
Assert.True(NHibernateUtil.IsInitialized(product.ProductModel));
}
Creamos un objecto DetachedCriteria por cada nivel que queremos cargar, definiéndole dentro un FetchMode para el nivel.
Posteriormente creamos el MultiCriteria añadiéndolo los ICriteria creados y lo ejecutamos con el método List(). En este ejemplo, result contiene 2 elementos, una lista de productos del resultado de ejecutar el primer DetachedCriteria y otra lista de productos por ejecutar el segundo DetachedCriteria.
La sentencia SQL generada es la siguiente:
SELECT ... FROM [Product] this_ left outer join SalesLT.[ProductModel] productmod2_ on this_.ProductModelID=productmod2_.ProductModelId WHERE this_.ProductId = @p0; SELECT ... FROM [Product] this_ left outer join SalesLT.SalesOrderDetail salesorder2_ on this_.ProductId=salesorder2_.ProductId WHERE this_.ProductId = @p1 @p0 = 707, @p1 = 707
La asociación de objetos de cada consulta la realiza nHibernate en memoria.
Carga temprana mediante Future<>
nHibernate nos da otra alternativa para realizar la consulta en código, para ello deberemos de usar el método Future<>(), ahí va un ejemplo:
[Fact]
public void CargaTempranaDeCategoriaPadreYModeloMetodo2()
{
using (ISession session = _sessionFactory.OpenSession())
{
IEnumerable<Product> result =
session.CreateCriteria<Product>()
.Add(Restrictions.Eq("ProductId", 680))
.SetFetchMode("ProductModel", FetchMode.Eager)
.Future<Product>();
session.CreateCriteria<Product>()
.Add(Restrictions.Eq("ProductId", 680))
.SetFetchMode("SalesOrderDetail", FetchMode.Eager)
.Future<Product>();
IEnumerator<Product> productEnumerator = result.GetEnumerator();
Assert.True(productEnumerator.MoveNext());
Assert.True(NHibernateUtil.IsInitialized(productEnumerator.Current.SalesOrderDetail));
Assert.True(NHibernateUtil.IsInitialized(productEnumerator.Current.ProductModel));
Assert.False(productEnumerator.MoveNext());
}
}
Si te fijas, nos quedamos únicamente con el resultado de la primera consulta, ignorando el resto de consultas. Otra diferencia respecto al ejemplo anterior es que aquí se nos devolverá una colección IEnumerable<> en vez de IList<>
El producto cartesiano en la unión de diferentes tablas sólo es problemático cuando la relación es de one-to-many. Cuando la relación es de many-to-one, podemos crear un único Criteria con todos los FetchMode de tipo many-to-one juntos y un Criteria por cada relación one-to-many o many-to-many que queramos precargar.
Carga temprana mediante LINQ
nHibernate también soporta LINQ para realizar consultas:
[Fact]
public void LinqCargaTempranaDeCategoriaPadre()
{
using (ISession session = _sessionFactory.OpenSession())
{
session.Linq<Product>().QueryOptions.RegisterCustomAction(
criteria => criteria.SetResultTransformer(new DistinctRootEntityResultTransformer()));
session.Linq<Product>()
.Expand("ProductModel")
.Where(p => p.ProductId == 680)
.ToList();
IList<Product> result = session.Linq<Product>()
.Expand("SalesOrderDetail")
.Where(p => p.ProductId == 680)
.ToList();
Assert.Equal(1, result.Count);
Assert.True(NHibernateUtil.IsInitialized(result[0].SalesOrderDetail), "SalesOrderDetail no inicializado");
Assert.True(NHibernateUtil.IsInitialized(result[0].ProductModel), "ProductModel no inicializado");
}
}
En esta ocasión, si que se realizan dos conexiones a la base de datos, una por cada consulta.
Si unas nHibernate 3 en tu proyecto, una de las mejoras en las consultas LINQ que han añadido es que trae programados nuevos métodos para la carga temprana de entidades.
El aspecto de la sentencia LINQ con nHibernate 3 sería:
var products = session.Query<Product>()
.FetchMany(c => c.SalesOrderDetail)
.ThenFetchMany(o => o.ProductModel).ToList();
Puedes leer más al respecto en el blog de Mike Hadlow:
http://mikehadlow.blogspot.com/2010/08/nhibernate-linq-eager-fetching.html