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>
,...) для управления в каждом конкретном случае пакетной загрузкой с отложенной загрузкой.
Это настроит ваши лениво загруженные сущности и коллекции сущностей не только для загрузки самих себя, но и для некоторых других ожидающих сущностей (того же класса) или коллекций сущностей (те же коллекции других сущностей того же класса).
Я написал подробное объяснение этого в этом другом ответе.