2

我在 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");
    }

我需要 Bookmarks 和 Tags 表中的数据。更具体地说:我需要 20 个带有相关标签的书签。我要做的第一件事是从 Bookmarks 表中选择 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 行。

当书签和标签表中有大量数据时,这会影响性能吗?我应该这样做吗?

4

2 回答 2

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 中修复笛卡尔积的示例,请参阅我的其他答案之一。

于 2013-11-07T21:02:24.223 回答
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。

这对您的数据模型和映射做出了相当多的假设,但我希望它说明了您可以使用的非常直接的优化。

于 2013-11-07T20:30:57.293 回答