我的同事问我是否给出了orderline示例,可以初始化一个看起来像这样的视图模型:
OrderViewModel
string OrderId
string CustomerName
List<OrderLineViewModel> OrderLines
OrderLineViewModel
string ProductName
string ROI
int Quantity
从索引?
我尝试过成功加载客户名称的转换,却无法从订单行获取相关的产品信息。这可以通过转换完成,还是需要从索引字段进行投影?
干杯,
詹姆斯
修改
我们正在尝试直接从查询中填充视图模型。 我们尝试了以下索引:
public class OrdersViewIndex : AbstractIndexCreationTask<Order>
{
Map = orders => from order in orders
select new {
OrderId = order.id
};
Transform = (database, orders) => from order in orders
let customer = database.Load<Customer>(order.customerId)
select new {
OrderId = order.id,
CustomerName = customer.Name,
OrderLines = // This is where I struggled to answer my colleagues questions as i'd need to load product name.
}
}
答案 0 :(得分:18)
首先,要意识到所有索引都会自动将Id
映射到名为__document_id
的索引条目中。因此再次映射它没有太大价值。您在此索引映射中所做的只是将其再次复制到另一个名为OrderId
的索引条目。
其次,要了解转换实际上并不是索引的一部分,而是仅附加到索引定义并在运行时执行。他们真正提供的是一种在服务器上变换查询结果的方法。在大多数情况下,这些都是你可以在客户端做的事情。
第三,索引用于查询非id字段,并提供possibly stale但eventually consistent结果。当您通过Id
(也称为文档密钥)检索文档时,根本没有必要使用索引。您希望使用.Load()
方法,它提供ACID保证,并且只从数据库中检索文档。
现在 - 您有一个问题,即当您的文档只有客户ID时如何获取客户名称,以及如何获取产品名称而不仅仅是产品ID。我们假设您的文档如下所示:
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; }
public List<OrderLine> OrderLines { get; set; }
}
public class OrderLine
{
public string ProductId { get; set; }
public int Quantity { get; set; }
}
public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
}
如果您使用其ID检索单个订单,则可以执行以下操作:
var order = session.Load<Order>(theOrderId);
但是现在你想要填充一些这样的视图模型:
public class OrderVM
{
public string OrderId { get; set; }
public string CustomerId { get; set; }
public string CustomerName { get; set; }
public List<OrderLineVM> OrderLines { get; set; }
}
public class OrderLineVM
{
public string ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
}
您可以使用Includes执行此操作。
var order = session.Include<Order>(x => x.CustomerId)
.Include<Order>(x => x.OrderLines.Select(y => y.ProductId))
.Load<Order>(theOrderId);
var orderViewModel = new OrderVM
{
OrderId = order.Id,
CustomerId = order.CustomerId,
CustomerName = session.Load<Customer>(order.CustomerId).Name,
OrderLines = order.OrderLines.Select(x => new OrderLineVM
{
ProductId = x.ProductId,
ProductName = session.Load<Product>(x.ProductId).Name,
Quantity = x.Quantity
})
};
尽管看到多次调用session.Load()
,但实际上只有一次调用数据库。 .Include
语句确保所有相关文档在第一次调用时加载到会话中。随后的调用只会将其拉出本地会话。
以上所有内容均用于通过ID检索单个订单。如果您想获得所有订单,或者获得特定客户的所有订单 - 然后,您需要查询。
对特定客户订单的动态查询如下所示:
var results = session.Query<Order>().Where(x => x.CustomerId == theCustomerId);
如果您想将这些投影到您的视图模型,就像之前可以使用includes:
var results = session.Query<Order>()
.Customize(x => x.Include<Order>(y => y.CustomerId)
.Include<Order>(y => y.OrderLines.Select(z => z.ProductId)))
.Where(x => x.CustomerId == theCustomerId)
.Select(x => new OrderVM
{
OrderId = x.Id,
CustomerId = x.CustomerId,
CustomerName = session.Load<Customer>(x.CustomerId).Name,
OrderLines = order.OrderLines.Select(y => new OrderLineVM
{
ProductId = y.ProductId,
ProductName = session.Load<Product>(y.ProductId).Name,
Quantity = y.Quantity
})
});
这确实有效,但您可能不想每次都写这个。此外,当您只需要每个产品的单个字段时,必须在会话中加载整个产品和客户记录。这是转换可能有用的地方。您可以按如下方式定义静态索引:
public class Orders_Transformed : AbstractIndexCreationTask<Order>
{
public Orders_Transformed()
{
Map = orders => from order in orders select new { };
TransformResults = (database, orders) =>
from order in orders
select new
{
OrderID = order.Id,
CustomerID = order.CustomerId,
CustomerName = database.Load<Customer>(order.CustomerId).Name,
OrderLines = order.OrderLines.Select(y => new
{
ProductId = y.ProductId,
ProductName = database.Load<Product>(y.ProductId).Name,
Quantity = y.Quantity
})
};
}
}
现在,当您查询时,转换已经为您设置了数据。您只需指定要投影到的结果VM。
var results = session.Query<Order, Orders_Transformed>().As<OrderVM>();
您可能已经注意到我在索引图中根本没有包含任何字段。那是因为我没有试图查询任何特定领域。所有数据都来自文档本身 - 索引中的唯一条目是自动添加的__document_id
条目,Raven使用这些条目来呈现文档存储中的数据 - 用于返回或转换。
现在假设我想通过其中一个相关字段进行查询。例如,我想获得名为Joe的客户的所有订单。为此,我需要在我的索引中包含客户名称。 RavenDB 2.0增加了一项功能,使这很容易 - Indexing Related Documents。
您需要修改索引图以使用LoadDocument
方法,如下所示:
Map = orders => from order in orders
select new
{
CustomerName = LoadDocument<Customer>(order.CustomerId)
};
如果您愿意,可以将其与“包含”或“变换”技术结合使用,以获取完整视图模型。
另一种技术是存储这些字段和project from the index。这对于CustomerName
这样的单个字段非常有效,但对于像OrderLines
这样的复杂值可能有点过分。
最后,另一种需要考虑的技术是denormalization。请考虑一下Product
可能更改名称或删除其名称。您可能不希望使先前的订单无效。将与订单相关的任何产品数据复制到OrderLine
对象中是个好主意。
public class OrderLine
{
public string ProductId { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
一旦这样做 - 您在检索订单时不再需要加载产品数据。变换部分变得不必要了,您将得到一个简单的索引投影,如下所示:
public class Orders_ByCustomerName : AbstractIndexCreationTask<Order>
{
public Orders_ByCustomerName()
{
Map = orders => from order in orders
select new
{
CustomerName = LoadDocument<Customer>(order.CustomerId).Name
};
Store("CustomerName", FieldStorage.Yes);
}
}
您可以使用以下内容轻松查询:
var results = session.Query<OrderVM, Orders_ByCustomerName>()
.Where(x => x.CustomerName == "Joe")
.As<OrderVM>();
请注意,在查询中,我第一次指定OrderVM
时,我正在定义索引条目的形状。它只是设置lambda,所以我可以指定x.CustomerName == "Joe"
。通常,您会看到用于此目的的特殊“结果”类。这无关紧要 - 我可以使用任何具有CustomerName
字符串字段的类。
当我指定.As<OrderVM>()
时 - 我实际上是从Order
类型转换为OrderVM
类型 - 而CustomerName
字段出现在我们的位置打开现场存储。
<强> TL; DR 强>
RavenDB有很多选择。尝试找到适合您需求的方法。正确的文档设计以及Indexing Related Documents与LoadDocument()
的谨慎使用将最有效地消除对索引转换的需求。