NHibernate: Почему Linq First () вызывает только один элемент во всех дочерних и внуковых коллекциях с FetchMany () - PullRequest
14 голосов
/ 17 июня 2011

Модель домена

У меня есть канонический домен 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 () возвращает Customer.

Редактировать 2011-06-17

После дальнейших исследований и размышлений, я считаю, что в зависимости от моего картографирования у этой методической цепочки есть два результата:

    .Where(c => c.CustomerID == id)
    .FetchMany(c => c.Orders)
    .ThenFetchMany(o => o.Items);

ПРИМЕЧАНИЕ: я не думаю, что могу получить поведение подвыбора, так как я не использую HQL.

  1. При отображении fetch="join" я должен получить декартово произведение между таблицами Customer, Order и OrderItem.
  2. Когда сопоставление fetch="select", я должен получить запрос для Customer, а затем несколько запросов для каждого Orders и OrderItems.

Как это работает с добавлением метода First () в цепочку, когда я теряю представление о том, что должно происходить.

Полученный запрос SQL является традиционным запросом левого внешнего соединения с select top (@p0) впереди.

Ответы [ 2 ]

16 голосов
/ 18 июня 2011

Метод First() переводится в SQL (по крайней мере, T-SQL) как SELECT TOP 1 .... В сочетании с выбором соединения это вернет одну строку, содержащую одного клиента, один заказ для этого клиента и одну позицию для заказа. Вы можете счесть это ошибкой в ​​Linq2NHibernate, но поскольку выборка из соединения встречается редко (и я думаю, что на самом деле вы снижаете свою производительность, используя одинаковые значения полей Customer и Order в сети как часть строки для каждого элемента), я сомневаюсь в команде это исправит.

То, что вы хотите - это один клиент, затем все заказы для этого клиента и все товары для всех этих заказов. Это происходит, когда NHibernate запускает SQL, который извлекает одну полную запись Customer (которая будет строкой для каждой строки заказа) и создает граф объекта 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 и модифицированного другими методами, выплевывая Enumerable в память, не перетаскивая его в конкретный список (NHibernate может, если пожелает, просто получить достаточно информации из DataReader для создания одного полного экземпляра верхнего уровня). Теперь метод First () больше не применяется к IQueryable для преобразования в SQL, но вместо этого он применяется к перечисляемому в памяти объектному графу, который после 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();

Для этого требуется запрос для каждого Заказа плюс два на уровне Клиента, и он не будет возвращать повторяющихся данных. Это будет работать намного лучше, чем отдельный запрос, возвращающий строку, содержащую каждое поле Заказчика и Заказа вместе с каждым Элементом, а также лучше, чем отправка запроса по Элементу плюс запрос по Заказу плюс запрос для Заказчика.

5 голосов
/ 13 июня 2013

Я хотел бы обновить ответ с моим найденным, чтобы он мог помочь кому-либо еще с такой же проблемой.

Поскольку вы запрашиваете у базы данных по их идентификатору, вы можете использовать .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 запроса, один для первого клиента, и позволить ленивой загрузке выполнить второй.

...