在RavenDB中从文档中查询集合中的信息的最佳方法是什么

时间:2013-04-04 14:46:58

标签: nosql ravendb

例如说我有以下课程

public class Parent
{
    public string Id { get; set; }
    public string Name { get; set; }

    public IList<Child> Children { get; set; }

    public class Child
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }
    }
}

以下文件

var document = new Parent
{
    Id = "parent/1",
    Name = "1",
    Children = new List<Parent.Child>
    {
        new Parent.Child {Date = new DateTime(2013, 1, 2)},
        new Parent.Child {Date = new DateTime(2013, 1, 3)},
        new Parent.Child {Date = new DateTime(2013, 1, 4)},
    }
};

我如何:

  1. 让RavenDB给我一个日期为2013/1/4的Child实例而不发送整个文件?
  2. 说明我的查询,以确保我做的正确吗?

1 个答案:

答案 0 :(得分:2)

简单的答案是你不想这样做。文档数据库的重点是文档代表整个聚合根实体(在域驱动设计术语中)。人们可以预期,在您所描述的文件中,所有“儿童”都与根“父母”相关。

对于一个真实世界的示例,请考虑具有LineItem的SalesOrder。从订单中只检索一行是否有意义?可能不是。您将检索整个SalesOrder - 包括所有LineItems。

这是一个需要理解的重要概念。在RDBMS(如SQL Server)中,您需要两个单独的表来存储订单及其订单项。要检索订单,您必须从SalesOrders表中读取1条记录,并从LineItems表中读取N条记录。这种反模式通常被称为“选择N + 1”,例如,当批量检索许多订单时,它会导致严重的可扩展性问题。

所以最好的答案是,根本不要查询。做一个简单的session.Load<Parent>("parents/1"),你也可以拥有所有的子数据,只需一次调用数据库。因此避免选择N + 1问题。

...

现在对于更复杂的答案 - 是的,你可以只获取文档的一部分。通常,您不会为单个文档执行此操作。您可能希望这样做以查询具有特定条件的多个文档。

例如,假设您要回答查询“给我所有具有特定日期的孩子”。这将是一个带索引投影的查询。

默认情况下,Raven适用于返回整个文档,而不是部分文档。换句话说,我们通常会回答相关问题“给我所有有特定日期的孩子的父母”。该查询如下所示:

var parents = session.Query<Parent>()
                     .Where(p=> p.Children.Any(c=> c.Date == theDate))
                     .ToList();

您可以获取此查询的结果,并在客户端使用linq-to-objects过滤掉您想要的子项(在上述ToList调用之后)

var children = parents.SelectMany(p=> p.Children.Where(c=> c.Date == theDate));

这会工作,但效率并不高,因为你从数据库中删除了很多数据。

另一种方法是使用带投影的静态索引在一次调用中执行此操作。首先,索引定义:

public class ChildrenIndex : AbstractIndexCreationTask<Parent>
{
    public ChildrenIndex()
    {
        Map = parents => from parent in parents
                         from child in parent.Children
                         select new
                         {
                             child.Date,
                             child.Name
                         };

        StoreAllFields(FieldStorage.Yes);
    }
}

然后查询:

session.Query<Parent.Child, ChildrenIndex>()
       .Where(x => x.Date == theDate)
       .AsProjection<Parent.Child>();

请注意,Parent.Child语法只是因为Child作为Parent的嵌套类。

在索引中,我们会映射我们想要投影的字段,然后我们存储这些字段,以便可以检索它们。我使用了StoreAllFields简写语法,但您也可以使用Store("FieldName", FieldStorage.Yes)分别标记每个字段。

在查询中,我第一次指定Parent.Child是因为我可以在Where谓词中查询它。单独留在这里,结果仍然是包含Parent。所以我们使用AsProjection来告诉乌鸦我们真的想要预测我们存储的字段,而不是回馈与索引术语匹配的文档。

...

现在希望你可以看到,从一个父母那里只返回一个孩子这样做有点过分了,你应该加载整个父母。

如果您发现自己经常加载特定的孩子,那么您的上下文中的孩子可能确实是自己的聚合,并且应该在自己的文档中。您仍然可以通过在子文档上存储ParentId或在父项上存储ChildrenIds列表或两者来关联父级和子级。