NHibernate lazy загружает вложенные коллекции фьючерсами, чтобы избежать проблемы N + 1

У меня есть объектная модель, которая выглядит так (псевдо-код):

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

Когда я загружаю данный продукт и хочу отображать изображения его рекомендаций, я сталкиваюсь с проблемой N + 1. (Рекомендации являются ленивыми, тогда цикл вызывает свойство .Images для каждого из них.)

Product -> Recommendations -> Images

То, что я хочу сделать, это загрузить эту определенную часть графика, но я не могу понять, как это сделать. Я могу загружать рекомендации с нетерпением, но не их изображения. Это то, что я пытался, но он не работает:

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

С помощью QueryOver, что является лучшим способом для этого? Я не хочу с нетерпением загружать изображения все время, только в этом конкретном сценарии.


EDIT. Я изменил свой подход, и хотя это не совсем то, что я имел в виду, он избегает проблемы N + 1. Теперь я использую два запроса: один для продукта и один для изображений его рекомендаций. Запрос продукта выполняется прямолинейно; вот запрос изображения:

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

Ответы

Ответ 1

JoinAlias - это еще один способ с нетерпением получить связанные записи, плюс мы можем использовать его, чтобы копать еще один уровень ниже Recommendations до Images. Мы будем использовать LeftOuterJoin, потому что мы хотим загрузить продукт, даже если у него нет рекомендаций.

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

При обсуждении жадных сборок нескольких коллекций с NHibernate, вы часто слышите, как люди упоминают декартовы продукты, но это не проблема. Если, однако, вы захотите загрузить следующий график...

 Product -> Recommendations -> Images
         -> Images

... then Product.Recommendations.Images X Product.Images формирует декартово произведение, которого следует избегать. Мы могли бы сделать так:

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;

Ответ 2

Принудительно загрузите часть графика, который вам нужен, используя класс NHibernateUtil.

 NHibernateUtil.Initialize(Product.Recommendations);

Подробнее см. ссылку ниже.

http://nhforge.org/wikis/howtonh/lazy-loading-eager-loading.aspx

Ответ 3

Если все, что вам нужно, - это избежать проблемы N + 1, используйте пакетную загрузку отложенных загрузок вместо активной загрузки.

Он устраняет проблемы N + 1, оказывая минимальное влияние на код: вам просто нужно изменить параметр конфигурации или настроить сопоставления.

В конфигурации установите default_batch_fetch_size в какое-то разумное значение для вашего обычного количества отложенных загрузок. 20 обычно хорошая ценность.

Или в сопоставлениях установите атрибуты batch-size для классов (<class>) и коллекций (<set>, <bag> ,...) для управления в каждом конкретном случае пакетной загрузкой с отложенной загрузкой.

Это настроит ваши лениво загруженные сущности и коллекции сущностей не только для загрузки самих себя, но и для некоторых других ожидающих сущностей (того же класса) или коллекций сущностей (те же коллекции других сущностей того же класса).

Я написал подробное объяснение этого в этом другом ответе.