NHibernate:为什么Linq First()只使用FetchMany()强制所有子孙集合中的一个项目

rbe*_*amy 15 nhibernate fetch eager-loading

领域模型

我有一个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;}
}
Run Code Online (Sandbox Code Playgroud)

订购

public class Order
{
  public Order()
  {
    Items = new HashSet<OrderItem>();
  }
  public virtual int Id {get;set;}
  public virtual Customer Customer {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

的OrderItems

public class OrderItem
{
  public virtual int Id {get;set;}
  public virtual Order Order {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

问题

无论是使用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];
Run Code Online (Sandbox Code Playgroud)

仅返回每个集合中的单个项目:

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

我想我理解这里发生了什么,这就是.First()方法被应用于前面的每个语句,而不仅仅是初始的.Where()子句.鉴于First()正在返回一个Customer,这对我来说似乎是不正确的行为.

编辑2011-06-17

经过进一步的研究和思考,我相信根据我的映射,这个方法链有两个结果:

    .Where(c => c.CustomerID == id)
    .FetchMany(c => c.Orders)
    .ThenFetchMany(o => o.Items);
Run Code Online (Sandbox Code Playgroud)

注意:我不认为我可以获得子选择行为,因为我没有使用HQL.

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

如何通过将第一个()方法添加到链中来解决应该发生的事情.

获取的SQL查询是传统的左外连接查询,select top (@p0)在前面.

Kei*_*thS 16

First()方法被翻译成SQL(至少T-SQL)为SELECT TOP 1 ....结合您的连接提取,这将返回一行,包含一个客户,一个客户订单和一个订单项.您可能会认为这是Linq2NHibernate中的一个错误,但由于连接提取很少(我认为您实际上会损害您的性能,在整个网络中将相同的Customer和Order字段值作为每个项目行的一部分)我怀疑团队会解决它.

您想要的是单个客户,然后是该客户的所有订单以及所有订单的所有订单.这可以通过让NHibernate运行SQL来提取一条完整的客户记录(这将是每个订单行的一行)并构建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();
Run Code Online (Sandbox Code Playgroud)

AsEnumerable()函数强制对Query创建的IQueryable进行评估,并使用其他方法进行修改,吐出内存中的Enumerable,而不将其插入具体的List中(NHibernate可以,如果它愿意,只需从中获取足够的信息) DataReader创建一个完整的顶级实例).现在,First()方法不再应用于要转换为SQL的IQueryable,而是应用于对象图的内存中的Enumerable,在NHibernate完成它之后,给定了Where子句,应该是零或一个客户记录与水合订单集合.

就像我说的那样,我认为你正在使用连接抓取来伤害自己.每行包含Customer的数据和Order的数据,并连接到每个不同的Line.这是很多冗余数据,我认为这将比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();
Run Code Online (Sandbox Code Playgroud)

这需要查询每个订单,以及客户级别的两个订单,并且不会返回任何重复数据.这将比返回包含Customer和Order的每个字段以及每个Item的行的单个查询执行得更好,并且还优于每个Item发送一个查询以及每个Order的查询以及Customer的查询.


ltv*_*van 7

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

由于您是根据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();
Run Code Online (Sandbox Code Playgroud)

这将生成一个带有where子句且没有TOP 1的普通SQL查询.

在其他情况下,如果结果有多个Customer,则会抛出异常,因此如果您确实需要基于条件的系列的第一项,它将无济于事.您必须使用2个查询,一个用于第一个Customer,而让延迟加载执行第二个查询.