我在ASP.NET MVC中创建一个网站,并使用NHibernate作为ORM。我的数据库中有以下表格:
映射:
public BookmarkMap()
{
Table("Bookmarks");
Id(x => x.Id).Column("Id").GeneratedBy.Identity();
Map(x => x.Title);
Map(x => x.Link);
Map(x => x.DateCreated);
Map(x => x.DateModified);
References(x => x.User, "UserId");
HasManyToMany(x => x.Tags).AsSet().Cascade.None().Table("TagsBookmarks").ParentKeyColumn("BookmarkId")
.ChildKeyColumn("TagId");
}
public TagMap()
{
Table("Tags");
Id(x => x.Id).Column("Id").GeneratedBy.Identity();
Map(x => x.Title);
Map(x => x.Description);
Map(x => x.DateCreated);
Map(x => x.DateModified);
References(x => x.User, "UserId");
HasManyToMany(x => x.Bookmarks).AsSet().Cascade.None().Inverse().Table("TagsBookmarks").ParentKeyColumn("TagId")
.ChildKeyColumn("BookmarkId");
}
我需要书签和标签表中的数据。更具体:我需要20个带有相关标签的书签。我要做的第一件事是从书签表中选择20个书签ID。我这样做是因为分页在我在第二个查询中得到的笛卡尔积不能很好地工作。
第一次查询:
IEnumerable<int> bookmarkIds = (from b in SessionFactory.GetCurrentSession().Query<Bookmark>()
where b.User.Username == username
orderby b.DateCreated descending
select b.Id).Skip((page - 1) * pageSize).Take(pageSize).ToList<int>();
之后我选择这些ID的书签。
第二次查询:
IEnumerable<Bookmark> bookmarks = (from b in SessionFactory.GetCurrentSession().Query<Bookmark>().Fetch(t => t.Tags)
where b.User.Username == username && bookmarkIds.Contains(b.Id)
orderby b.DateCreated descending
select b);
我使用fetch的原因是因为我想避免N + 1个查询。这有效,但会产生笛卡尔积。我在一些帖子中读到你应该避免使用笛卡尔产品,但我不知道如何在我的情况下这样做。
我还读过有关为N + 1查询设置批量大小的内容。这真的比这个查询更快吗?
用户可以将最多5个标签添加到书签。我每页选择20个书签,因此第二个查询的最坏情况是:5 * 20 = 100行。
当我在书签和标签表中有大量数据时,这会影响性能吗?我应该采用不同的方式吗?
答案 0 :(得分:1)
这不是笛卡尔积。
〜图A~
Bookmarks -> Tags -> Tag
笛卡尔积是两种不同组合的所有可能组合。例如,假设我们有三个表:Customer,CustomerAddress和CustomerEmail。客户有很多地址,他们也有很多电子邮件地址。
〜图B~
Customers -> Addresses -> Emails
如果你写了像......这样的查询。
select *
from
Customer c
left outer join CustomerAddress a
on c.Id = a.Customer_id
left outer join CustomerEmail e
on c.Id = e.Customer_id
where c.Id = 12345
...而且此客户有5个地址和5个电子邮件地址,您最终会返回5 * 5 = 25
行。你可以看出为什么这对性能有害。这是不必要的数据。了解客户的地址和电子邮件地址的每种可能组合都告诉我们没有用处。
使用您的查询,您不会返回任何不必要的行。结果集中的每一行都直接对应于您感兴趣的其中一个表中的行,反之亦然。没有乘法。相反,你有TagsBookmarksCount + BookmarksThatDontHaveTagsCount
。
查找笛卡尔积的关键位置是当您的查询分支为两个单独的不相关集合时。如果您只是深入挖掘单个子集合链,如图A ,则没有笛卡尔积。查询返回的行数将受该最深集合返回的行数限制。一旦你分支到一边,你现在在查询中有两个并行的并排集合,如图B ,那么你有一个笛卡尔积,结果将是不必要地增加了。
要修复笛卡尔积,请将查询拆分为多个查询,以便添加行数,而不是相乘。使用NHibernate的Future
方法,您可以将这些单独的查询一起批处理,因此您仍然只能往返数据库。有关如何在NHibernate中修复笛卡尔积的示例,请参阅one of my other answers。
答案 1 :(得分:0)
Query<>.Fetch()
旨在确保正在进行急切加载,并且当它是一对多关系时,因为这似乎是(即如果Bookmark.Tags
是一个集合)那么这两个你对此的看法大致相同。如果Tags
是延迟加载的并且只是很少访问,那么将其保留为非获取可能是最好的方法(如在第一个查询中),因为您不会总是访问标签。这取决于用例。
另一方面,如果你知道你将永远获得所有标签,那么将其分解为另一个查询可能更有意义,这次是Tags
类型/表是什么,查看它们而不是使用NHibernate关系来完成这项工作。
如果Tag
具有书签的外键,例如BookmarkId
,则ToLookup在这种情况下非常有用:
var tagLookup = (from t in SessionFactory.GetCurrentSession().Query<Tag>()
// limit query appropriately for all the bookmarks you need
// this should be done once, in this optimization
select new {key=t.BookmarkId, value=t} )
.ToLookup(x=>x.key, x=>x.value);
将为您提供查找(ILookup<int, Tag>
),您可以在其中执行以下操作:
IGrouping<Tag> thisBookmarksTags = tagLookup[bookmarkId];
这将为您提供该书签所需的标签。这将它分成另一个查询,从而避免N + 1。
这对您的数据模型和映射做了很多假设,但我希望它能说明您可以使用的非常简单的优化。