使用NHibernate 3.0.0时如何解决笛卡尔积(x-join)问题

32

我数学不好,但是我大概知道笛卡尔积是什么。
这是我的情况(简化):

public class Project{
 public IList<Partner> Partners{get;set;}
}
public class Partner{
 public IList<PartnerCosts> Costs{get;set;}
 public IList<Address> Addresses{get;set;}
}
public class PartnerCosts{
 public Money Total{get;set;}
}
public class Money{
 public decimal Amount{get;set;}
 public int CurrencyCode{get;set;}
}
public class Address{
 public string Street{get;set;}
}

我的目标是有效地加载整个项目。

问题当然是:

  • 如果我尝试急切地加载合作伙伴及其成本,查询会返回无数行
  • 如果我惰性地加载Partner.Costs,数据库会收到请求垃圾邮件(这比第一种方法稍微快一点)

根据我的阅读,常见的解决办法是使用MultiQueries,但我有点不理解。
因此,我希望通过这个具体的例子来学习。

如何有效地加载整个项目?

P.s. 我正在使用NHibernate 3.0.0。
请不要发布使用HQL或字符串风格的Criteria API方法的答案。


2
我认为你在这里不会得到笛卡尔积。你的结构是Project-1:n-Partner-1:n-PartnerCosts-1:1-Money。因此,你在结果中得到的行数将始终是count(PartnerCosts)。如果你在Partner类中有另一个IList<Something>并尝试在同一查询中加载它,那么你将得到count(Something)*count(PartnerCosts)。由于你不想使用ICriteria或HQL,你最好的选择是QueryOver with Futures。如果到时候没有其他人写出示例,我会稍后写一个并将其发布为答案。 - Florian Lim
@Florian,就像我说的一样 - 我不擅长数学。我稍微修正了我的理解,并为合作伙伴添加了“地址”。使用QueryOver将是完美的。 - Arnis Lapsa
请帮忙。这对我没有用,我需要看看你是如何做的".JoinAlias(p => p.Project, () => pAlias)"当项目类上没有属性时???你使用的类与问题中发布的类完全相同吗?p.Project怎么编译通过的? - joncodo
@JonathanO “当项目类中没有项目属性时” - 怀疑项目是否应该自我引用。如果您的意思是“合作伙伴没有对项目的引用”,那么恐怕这种解决笛卡尔积问题的方法行不通。但我真的不记得了,怀疑我能否提供帮助。 :) - Arnis Lapsa
加入我的聊天室,这里是链接:http://chat.stackoverflow.com/rooms/6667/question - joncodo
4个回答

47

好的,我为自己写了一个反映你结构的示例,这应该可以工作:

int projectId = 1; // replace that with the id you want
// required for the joins in QueryOver
Project pAlias = null;
Partner paAlias = null;
PartnerCosts pcAlias = null;
Address aAlias = null;
Money mAlias = null;

// Query to load the desired project and nothing else    
var projects = repo.Session.QueryOver<Project>(() => pAlias)
    .Where(p => p.Id == projectId)
    .Future<Project>();

// Query to load the Partners with the Costs (and the Money)
var partners = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(p => p.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Costs, () => pcAlias)
    .JoinAlias(() => pcAlias.Money, () => mAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// Query to load the Partners with the Addresses
var partners2 = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(o => o.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Addresses, () => aAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// when this is executed, the three queries are executed in one roundtrip
var list = projects.ToList();
Project project = list.FirstOrDefault();
我的类有不同的名称,但反映了完全相同的结构。我更改了这些名称,希望没有拼写错误。
解释: 这些别名是用于连接所需的。 我定义了三个查询来加载您想要的“Project”,带有其“Costs”的“Partners”以及带有其“Addresses”的“Partners”。 通过使用“.Futures()”,我基本上告诉NHibernate在我实际想要结果时,在一次往返中执行它们,使用“projects.ToList()”。 这将导致三个SQL语句在一次往返中执行。 这三个语句将返回以下结果: 1)包含您的项目的1行 2)x行与合作伙伴及其成本(和资金)相关,其中x是项目合作伙伴的总成本数 3)y行与合作伙伴及其地址相关,其中y是每个项目合作伙伴的地址总数
您的数据库应该返回1 + x + y行,而不是x * y行,这将是笛卡尔积。 我确实希望您的数据库支持该功能。

1
@JonathanO 很抱歉回复晚了。p.Project 可以编译通过,因为 p.Project 不是 Project 类的属性,而是 Partner 类的属性。在表达式 .JoinAlias(p => p.Project, () => pAlias) 中,p 引用的是 .QueryOver<Partner> 方法中的类。 - Florian Lim
1
@Jacko,在原始问题中没有提到,但在我的示例代码中,我有一个从PartnerProject的引用(在类和映射中)。简而言之,在Project类中有一个public IList<Partner> Partners {get; set;},而在Partner类中我们有public Project Project {get; set;}。映射文件相应地如下所示。 - Florian Lim
@Florian 如果有一个条件,例如PartnerCosts.Total.Amount应该大于$0,你会把它放在哪里?放在“projects”查询中还是全部三个查询中都要放? - Dharmesh
@Dharmesh 如果你的目标是“加载ID为x的项目,仅包括Amount大于0的PartnerCosts”,那么我建议将其放在第二个查询(partners)中,而不是所有三个查询中。我没有测试代码了,所以无法自行测试。 - Florian Lim
我今天尝试了一下,发现需要进行一些更改(可能是因为本文所述的内容与 NHibernate 的更新有关)。第一个查询需要主动加载合作伙伴,否则 NHibernate 将会额外发出一条查询语句:var projects = repo.Session.QueryOver<Project>(() => pAlias).Where(p => p.Id == projectId).Fetch(p=>p.Projects()).Eager.Future<Project>(); - Konstantin
显示剩余5条评论

5
如果您在NHibernate上使用Linq,可以使用以下方法简化笛卡尔积预防:
int projectId = 1;
var p1 = sess.Query<Project>().Where(x => x.ProjectId == projectId);


p1.FetchMany(x => x.Partners).ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Costs)
    .ThenFetch(x => x.Total)
.ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Addresses)
.ToFuture();


Project p = p1.ToFuture().Single();

这里有详细的解释:http://www.ienablemuch.com/2012/08/solving-nhibernate-thenfetchmany.html

该链接介绍了如何解决NHibernate的ThenFetchMany方法。

这个方法是可行的,但仅因为我们在项目的一个子集合上(因此,FetchMany(x => x.Partners) 是可以的)。如果有第二个子集合(比如贡献者之类的),这仍然会导致笛卡尔积,因为你需要在项目上为两个子集合都调用 FetchMany。似乎 NH LINQ 没有足够聪明的方式将子集合的未来查询与根实体联系起来,就像 QueryOver 显然可以做到的那样。请参见:https://dev59.com/GFXTa4cB1Zd3GeqP25Xw#5435464 - kdawg
@kdawg是Contributors的子级还是Partners的子级?我想要重现那个笛卡尔积。 - Michael Buen
一个项目的子项。实际上,我想再次访问这个问题。最初,我有类似于“sess.Query<Project>().FetchMany(x => x.Partners).FetchMany(x => x.Contributers).ToFuture()”的东西,自然产生了一个笛卡尔积项目。从那时起,我进行了NH Futures的崩溃课程,并最终得到了与您上面所述非常相似的东西,但使用了QueryOver API。最初,我认为LINQ futures不能做我想要做的事情(类似于这个问题),但现在我不太确定了!=) - kdawg
我找到的第一个确实起作用的Linq示例。谢谢!! - Simon Fox

2

不要使用急切加载多个集合并得到一个令人讨厌的笛卡尔积:

Person expectedPerson = session.Query<Person>()
    .FetchMany(p => p.Phones)
        .ThenFetch(p => p.PhoneType)
    .FetchMany(p => p.Addresses)
    .Where(x => x.Id == person.Id)
    .ToList().First();

您应该将子对象批量处理在一个数据库调用中:
// create the first query
var query = session.Query<Person>()
      .Where(x => x.Id == person.Id);
// batch the collections
query
   .FetchMany(x => x.Addresses)
   .ToFuture();
query
   .FetchMany(x => x.Phones)
   .ThenFetch(p => p.PhoneType)
   .ToFuture();
// execute the queries in one roundtrip
Person expectedPerson = query.ToFuture().ToList().First();

我刚刚写了一篇关于如何使用Linq、QueryOver或HQL避免这种情况的博客文章。

http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies/

已更改,请升级我的帖子。 - Raffaeu
太棒了,我在一个老项目上卡住了NHibernate 3.x(我们使用Visual Studio 2008,天哪)。由于恶性bug的存在,我甚至无法使用带有ToFuture的Query方法,只能使用QueryOver。你的文章救了我的一天。 - Tiago César Oliveira

1
我只是想为Florian的非常有帮助的答案做出贡献。我通过艰辛的方式发现,所有这些的关键都在于别名。别名确定了进入sql的内容,并被NHibernate用作“标识符”。成功加载三级对象图的最小Queryover如下:
Project pAlias = null;
Partner paAlias = null;

IEnumerable<Project> x = session.QueryOver<Project>(() => pAlias)
 .Where(p => p.Id == projectId)
 .Left.JoinAlias(() => pAlias.Partners, () => paAlias)
 .Future<Project>();


session.QueryOver(() => paAlias).Fetch(partner => partner.Costs).
 .Where(partner => partner.Project.Id == projectId)
 .Future<Partner>();

第一个查询加载项目及其子合作伙伴。重要的部分是合作伙伴的别名。 合作伙伴别名用于命名第二个查询。第二个查询加载合作伙伴和成本。 当这被执行为“多查询”时,Nhibernate将“知道”第一个和第二个查询是由paAlias连接的 (或者更确切地说,生成的sql将具有“相同”的列别名)。 因此,第二个查询将继续加载已在第一个查询中开始的合作伙伴。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接