2011-03-06 19 views
13

tengo un modelo de objetos que tiene este aspecto (pseudo código):NHibernate carga diferida anidado colecciones con futuros para evitar N + 1 problema

class Product { 
    public ISet<Product> Recommendations {get; set;} 
    public ISet<Product> Recommenders {get; set;} 
    public ISet<Image> Images {get; set; } 
} 

Cuando cargo un producto dado y quiero mostrar las imágenes de sus recomendaciones, me encuentro con un problema N + 1. (Las recomendaciones son perezosos-cargado, a continuación, un bucle llama a la propiedad .Images de cada uno.)

Product -> Recommendations -> Images 

Lo que quiero hacer es cargar con impaciencia esta parte particular de la gráfica, pero no puedo averiguar cómo hacerlo. Puedo cargar las recomendaciones con entusiasmo, pero no sus imágenes. Esto es lo que he estado tratando, pero no parece funcionar:

//get the IDs of the products that will be in the recommendations collection 
var recommendedIDs = QueryOver.Of<Product>() 
    .Inner.JoinQueryOver<Product>(p => p.Recommenders) 
    .Where(r => r.Id == ID /*product we are currently loading*/) 
    .Select(p => p.Id); 

//products that are in the recommendations collection should load their 
//images eagerly 
CurrentSession.QueryOver<Product>() 
    .Fetch(p => p.Images).Eager 
    .Where(Subqueries.WhereProperty<Product>(p => p.Id).In(recommendedIDs)) 
    .Future<Product>(); 

//load the current product 
return CurrentSession.QueryOver<Product>() 
    .Where(p => p.Id == ID); 

Usando QueryOver, ¿cuál es la mejor manera de lograr esto? No quiero cargar imágenes con entusiasmo todo el tiempo, solo en este escenario en particular.


EDITAR: He cambiado mi enfoque, y aunque no es exactamente lo que tenía en mente, lo hace de evitar el problema de N + 1. Ahora estoy usando dos consultas, una para el producto y otra para las imágenes de sus recomendaciones. La consulta del producto es directa; Ésta es la consulta de imagen:

//get the recommended product IDs; these will be used in 
//a subquery for the images 
var recommendedIDs = QueryOver.Of<Product>() 
    .Inner.JoinQueryOver<Product>(p => p.Recommenders) 
    .Where(r => r.Id == RecommendingProductID) 
    .Select(p => p.Id); 

//get the logo images for the recommended products and 
//create a flattened object for the data 
var recommendations = CurrentSession.QueryOver<Image>() 
    .Fetch(i => i.Product).Eager 
    /* filter the images down to only logos */ 
    .Where(i => i.Kind == ImageKind.Logo) 
    .JoinQueryOver(i => i.Product) 
    /* filter the products down to only recommendations */ 
    .Where(Subqueries.WhereProperty<Product>(p => p.Id).In(recommendedIDs)) 
    .List().Select(i => new ProductRecommendation { 
     Description = i.Product.Description, 
     ID = i.Product.Id, 
     Name = i.Product.Name, 
     ThumbnailPath = i.ThumbnailFile 
    }).ToList(); 

return recommendations; 

Respuesta

17

JoinAlias es otra manera a buscar ansiosamente los registros relacionados, además de que se puede utilizar para cavar otro nivel más profundo a través Recommendations a Images. Usaremos LeftOuterJoin porque queremos cargar el producto incluso si no tiene recomendaciones.

Product recommendationAlias = null; 
Image imageAlias = null; 

return CurrentSession.QueryOver<Product>() 
    .JoinAlias(x => x.Recommendations,() => recommendationAlias, JoinType.LeftOuterJoin) 
    .JoinAlias(() => recommendationAlias.Images,() => imageAlias, JoinType.LeftOuterJoin) 
    .Where(x => x.Id == ID) 
    .TransformUsing(Transformers.DistinctRootEntity) 
    .SingleOrDefault(); 

Cuando se habla de recuperación temprana de varias colecciones con NHibernate, se oye a menudo la gente menciona los productos cartesianos, pero eso no es una preocupación en este caso. Sin embargo, si usted deseaba para cargar el gráfico siguiente vez ...

Product -> Recommendations -> Images 
     -> Images 

... entonces Product.Recommendations.Images X Product.Images formarían un producto cartesiano que debemos evitar. Podríamos hacerlo de esta manera:

Product recommendationAlias = null; 
Image imageAlias = null; 

var productFuture = CurrentSession.QueryOver<Product>() 
    .JoinAlias(x => x.Recommendations,() => recommendationAlias, JoinType.LeftOuterJoin) 
    .JoinAlias(() => recommendationAlias.Images,() => imageAlias, JoinType.LeftOuterJoin) 
    .Where(x => x.Id == ID) 
    .TransformUsing(Transformers.DistinctRootEntity) 
    .FutureValue(); 

var imagesFuture = CurrentSession.QueryOver<Product>() 
    .Fetch(x => x.Images).Eager 
    .Where(x => x.Id == ID) 
    .TransformUsing(Transformers.DistinctRootEntity) 
    .Future(); 

return productFuture.Value; 
+0

¡He estado buscando 'TransformUsing' durante 2 horas ahora! Muchas gracias .. –

+0

¡No hay problema! Me alegro de poder ayudar. –

0

Si todo lo que quiere es evitar la molestia N + 1, el uso de cargas Batch fetching perezosos en lugar de cargar con ganas.

Elimina N + 1 problemas mientras tiene un impacto mínimo en el código: solo tiene que cambiar un parámetro de configuración o ajustar las asignaciones.

En la configuración, configure default_batch_fetch_size a un valor razonable para sus conteos de cargas perezosas habituales. 20 es generalmente un buen valor.

O en asignaciones, defina los atributos batch-size en clases (<class>) y colecciones (<set>, <bag>, ...) para controlar caso por caso la dosificación perezoso carga.

Esto configurará sus entidades y colecciones de entidades cargadas de forma perezosa para que no solo se carguen, sino también otras que esperan entidades (de la misma clase) o colecciones de entidades (mismas colecciones de otras entidades padre).

He escrito una explicación detallada de él en this other answer.

Cuestiones relacionadas