Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
213 views
in Technique[技术] by (71.8m points)

queryover - NHibernate lazy loading nested collections with futures to avoid N+1 problem

I have an object model that looks like this (pseudo code):

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

When I load a given product and want to display the images of its recommendations, I run into an N+1 problem. (The recommendations are lazy-loaded, then a loop calls the .Images property of each one.)

Product -> Recommendations -> Images

What I want to do is eagerly load this particular part of the graph, but I can't figure out how to do it. I can load the recommendations eagerly, but not their images. This is what I have been trying, but it doesn't seem to work:

//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);

Using QueryOver, what is the best way to accomplish this? I don't want to eagerly load images all the time, just in this particular scenario.


EDIT: I have changed my approach, and while it's not exactly what I had in mind, it does avoid the N+1 problem. I am now using two queries, one for the product, and one for the images of it's recommendations. The product query is straight-forward; here is the image query:

//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;
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

JoinAlias is another way to eagerly fetch related records, plus we can use it to dig another level deeper through Recommendations down to Images. We'll use LeftOuterJoin because we want to load the product even if it has no recommendations.

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();

When discussing eager fetching of multiple collections with NHibernate, you often hear people mention Cartesian products, but that's not a concern here. If however, you wished to load the following graph instead...

 Product -> Recommendations -> Images
         -> Images

... then Product.Recommendations.Images X Product.Images would form a Cartesian product that we should avoid. We could do so like this:

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;

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...