NHibernate:为什么LinqFirst()使用FetchMany()在所有子集合和孙集合中只强制一个项



域模型

我得到了一个具有许多OrdersCustomer的规范域,每个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;}
}

问题

无论是用FluentHibernate还是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);

注意:我不认为我可以得到subselect行为,因为我没有使用HQL。

  1. 当映射为fetch="join"时,我应该获得Customer、Order和OrderItem表之间的笛卡尔乘积
  2. 当映射为fetch="select"时,我应该获得一个Customer查询,然后分别获得多个Orders和OrderItems查询

在将First()方法添加到链中的过程中,我会忘记应该发生什么。

get发出的SQL查询是传统的左外联接查询,前面有select top (@p0)

First()方法被转换为SQL(至少T-SQL)作为SELECT TOP 1 ...。结合您的联接获取,这将返回一行,其中包含一个客户、该客户的一个订单和该订单的一个项目。您可能会认为这是Linq2NHibernate中的一个错误,但由于获取联接的情况很少见(我认为,在整个网络中,作为每个Item行的一部分,提取相同的Customer和Order字段值实际上会损害您的性能),我怀疑团队是否会修复它。

您想要的是一个客户,然后是该客户的所有订单和所有这些订单的所有项目。这是通过让NHibernate运行SQL来实现的,SQL将提取一个完整的Customer记录(每个订单行将是一行)并构建Customer对象图。将Enumerable转换为List,然后获得第一个元素是可行的,但以下操作会稍微快一点:

var customer = this.generator.Session.Query<Customer>()
    .Where(c => c.CustomerID == id)
    .FetchMany(c => c.Orders)
    .ThenFetchMany(o => o.Items)
    .AsEnumerable().First();

AsEnumerable()函数强制计算Query创建并用其他方法修改的IQueryable,在内存中吐出一个Enumerable,而不将其拖入具体列表(如果愿意,NHibernate可以简单地从DataReader中提取足够的信息来创建一个完整的顶级实例)。现在,First()方法不再应用于要转换为SQL的IQueryable,而是应用于对象图的内存中的Enumerable,在NHibernate完成任务并给定Where子句后,该对象图应该是零或一个具有水合Orders集合的Customer记录。

就像我说的,我认为你在使用连接获取来伤害自己。每一行都包含客户的数据和订单的数据,并连接到每个不同的行。这是大量冗余数据,我认为这将比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和Order的每个字段以及每个Item的行要好得多,也比按Item发送查询加按Order发送查询加为Customer发送查询要好得多。

我想用我的发现更新答案,这样可以帮助其他有同样问题的人。

由于您是根据实体的ID查询实体,因此可以使用.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();

这将生成一个带有where子句但不带TOP 1的普通SQL查询。

在另一种情况下,如果结果有多个Customer,则会抛出异常,所以如果您真的需要基于条件的系列的第一项,则这将没有帮助。您必须使用两个查询,一个用于第一个Customer,让惰性负载执行第二个查询。

相关内容

  • 没有找到相关文章

最新更新