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 запроса, один для первого клиента, и пусть ленивая загрузка сделает вторую.