NHibernate: Почему Linq First() заставляет только один элемент во всех дочерних и внукальных коллекциях с FetchMany()
Модель домена
У меня есть канонический домен a Customer
со многими Orders
, причем каждый Order
имеет много OrderItems
:
Клиент
public class Customer
{
public Customer()
{
Orders = new HashSet<Order>();
}
public virtual int Id {get;set;}
public virtual ICollection<Order> Orders {get;set;}
}
Заказ
public class Order
{
public Order()
{
Items = new HashSet<OrderItem>();
}
public virtual int Id {get;set;}
public virtual Customer Customer {get;set;}
}
ТоварыЗаказа
public class OrderItem
{
public virtual int Id {get;set;}
public virtual Order Order {get;set;}
}
Проблема
Независимо от того, сопоставлен ли с файлами FluentNHibernate или hbm, я запускаю два отдельных запроса, которые идентичны в их синтаксисе Fetch(), за исключением одного, включающего метод расширения .First().
Возвращает ожидаемые результаты:
var customer = this.generator.Session.Query<Customer>()
.Where(c => c.CustomerID == id)
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.Items).ToList()[0];
Возвращает только один элемент в каждой коллекции:
var customer = this.generator.Session.Query<Customer>()
.Where(c => c.CustomerID == id)
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.Items).First();
Я думаю, что понимаю, что происходит здесь, а именно, что метод .First() применяется к каждому из предыдущих утверждений, а не только к исходному предложению .Where(). Это кажется неправильным поведением для меня, учитывая тот факт, что First() возвращает клиента.
Изменить 2011-06-17
После дальнейших исследований и мышления я считаю, что в зависимости от моего сопоставления в эту цепочку методов есть два результата:
.Where(c => c.CustomerID == id)
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.Items);
ПРИМЕЧАНИЕ. Я не думаю, что могу получить подзапрос, поскольку я не использую HQL.
- Когда отображение
fetch="join"
, я должен получить декартовое произведение между таблицами Customer, Order и OrderItem.
- Когда отображение
fetch="select"
, я должен получить запрос для Клиента, а затем несколько запросов для Заказов и OrderItems.
Как это происходит с добавлением метода First() в цепочку, я теряю информацию о том, что должно произойти.
Вывод SQL Query, который выдается, представляет собой традиционный запрос левого внешнего соединения с select top (@p0)
спереди.
Ответы
Ответ 1
Метод First()
преобразуется в SQL (по крайней мере, T-SQL) как SELECT TOP 1 ...
. В сочетании с вашей попыткой присоединения, это вернет одну строку, содержащую одного клиента, один заказ для этого клиента и один элемент для заказа. Вы можете подумать об этом как об ошибке в Linq2NHibernate, но поскольку сборка ссылок встречается редко (и я думаю, что вы на самом деле вредите своей производительности, вытягивая одни и те же значения поля "Заказчик" и "Заказ" по сети, как часть строки для каждого элемента). Я сомневаюсь, что команда исправит его.
То, что вы хотите, это один Клиент, затем все Заказы для этого клиента и все элементы для всех этих Заказов. Это происходит, позволяя NHibernate запускать SQL, который вытащит одну полную запись клиента (которая будет строкой для каждой строки заказа) и построит график объекта Customer. Превращение Перечислимого в список, а затем получение первого элемента работает, но следующее будет немного быстрее:
var customer = this.generator.Session.Query<Customer>()
.Where(c => c.CustomerID == id)
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.Items)
.AsEnumerable().First();
функция AsEnumerable() заставляет оценивать IQueryable, созданный Query, и модифицировать с помощью других методов, выплевывая In-memory Enumerable, не вставляя ее в конкретный список (NHibernate может, если захочет, просто вытащить достаточную информацию из DataReader, чтобы создать один полный экземпляр верхнего уровня). Теперь метод First() больше не применяется к IQueryable для перевода в SQL, но вместо этого применяется к встроенной памяти Enumerable из графиков объектов, которая после того, как NHibernate выполнила свою работу, и с учетом предложения Where, должна быть нулевой или одной записи клиента с гидратированной коллекцией Заказов.
Как я уже сказал, я думаю, что вы причиняете себе боль, пользуясь подключением. Каждая строка содержит данные для Клиента и данные для заказа, соединенные с каждой отдельной линией. Это много избыточных данных, которые, по моему мнению, будут стоить вам больше, чем даже стратегия запросов N + 1.
Лучший способ, с помощью которого я могу справиться, - это один запрос на объект для извлечения этого объекта. Это будет выглядеть так:
var session = this.generator.Session;
var customer = session.Query<Customer>()
.Where(c => c.CustomerID == id).First();
customer.Orders = session.Query<Order>().Where(o=>o.CustomerID = id).ToList();
foreach(var order in customer.Orders)
order.Items = session.Query<Item>().Where(i=>i.OrderID = order.OrderID).ToList();
Для этого требуется запрос для каждого ордера, плюс два на уровне Customer и не будет возвращать дубликаты данных. Это будет намного лучше, чем один запрос, возвращающий строку, содержащую все поля Клиента и Ордера вместе с каждым элементом, а также лучше, чем отправка запроса на элемент плюс запрос за заказ плюс запрос для Клиента.
Ответ 2
Я хочу обновить ответ с помощью моего поиска, чтобы он мог помочь кому-либо другому с той же проблемой.
Поскольку вы запрашиваете базу сущностей по их идентификатору, вы можете использовать .Single вместо .First или .AsEnumerable(). First():
var customer = this.generator.Session.Query<Customer>()
.Where(c => c.CustomerID == id)
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.Items).Single();
Это создаст обычный SQL-запрос с предложением where и без TOP 1.
В другой ситуации, если результат имеет более одного Клиента, будет выбрано исключение, поэтому оно не поможет, если вам действительно нужен первый элемент серии, основанный на условии. Вы должны использовать 2 запроса, один для первого клиента, и пусть ленивая загрузка сделает вторую.