NHibernate: почему Linq First () принудительно использует только один элемент во всех дочерних и внучатых коллекциях с помощью FetchMany ()

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

У меня есть канонический домен 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;}
}

OrderItems

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", я должен получить запрос для клиента, а затем несколько запросов каждый для заказов и элементов заказа.

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

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


person rbellamy    schedule 17.06.2011    source источник


Ответы (2)


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

Вам нужен один клиент, затем все заказы для этого клиента и все элементы для всех этих заказов. Это происходит, когда NHibernate запускает SQL, который извлечет одну полную запись Customer (которая будет строкой для каждой строки заказа) и построит граф объекта Customer. Превращение Enumerable в список с последующим получением первого элемента работает, но следующее будет немного быстрее:

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, а вместо этого применяется к 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();

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

person KeithS    schedule 17.06.2011

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

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

person ltvan    schedule 13.06.2013
comment
Я думаю, что это должен быть правильный ответ, поскольку, как вы говорите, проблема в том, что .first () добавляет ТОП 1. - person Maximiliano Becerra; 18.07.2017