DDD:实体的集合和存储库

假设我有

public class Product: Entity { public IList Items { get; set; } } 

假设我想找到一个带有最大值的项…我可以添加方法Product.GetMaxItemSmth()并使用Linq( from i in Items select i.smth).Max() )或手动循环或其他。 现在,问题是这会将完整的集合加载到内存中。

正确的解决方案是执行特定的数据库查询,但域实体无权访问存储库,对吧? 所以我要么

 productRepository.GetMaxItemSmth(product) 

(这很丑陋,没有?),或者即使实体有权访问存储库,我也使用来自实体的IProductRepository

 product.GetMaxItemSmth() { return Service.GetRepository().GetMaxItemSmth(); } 

这也是丑陋的,是代码的重复。 我甚至可以去看看并进行扩展

 public static IList GetMaxItemSmth(this Product product) { return Service.GetRepository().GetMaxItemSmth(); } 

哪个更好只是因为它没有真正使存储库的实体混乱…但仍然是方法重复。

现在,这是否是再次使用product.GetMaxItemSmth()productRepository.GetMaxItemSmth(product) 。 我错过了DDD的东西吗? 这里的正确方法是什么? 只需使用productRepository.GetMaxItemSmth(product) ? 这是每个人都使用和满意的吗?

我觉得这不对…如果我不能从产品本身访问产品的Items ,为什么我在Product中需要这个集合? 然后,如果Product无法使用特定查询并在没有性能命中的情况下访问其集合,那么Product可以做任何有用的事吗?

当然,我可以使用效率较低的方式而不用介意,当它很慢时,我会将存储库调用作为优化注入实体……但即使这听起来也不对,是吗?

有一点要提一下,也许它不是DDD ……但我需要在Product中使用IList才能获得使用Fluent NHibernate生成的数据库模式。 不过,请在纯DDD环境中自由回答。

更新:这里描述了一个非常有趣的选项: http : //devlicio.us/blogs/billy_mccafferty/archive/2007/12/03/custom-collections-with-nhibernate-part-i-the-basics.aspx ,不仅处理与DB相关的集合查询,也可以帮助进行集合访问控制。

拥有Items集合具有GetXXX()方法都是正确的。

为了纯粹,您的实体不应该直接访问存储库。 但是,它们可以通过查询规范进行间接引用。 查看Eric Evans的书第229页。 像这样的东西:

 public class Product { public IList Items {get;} public int GetMaxItemSmth() { return new ProductItemQuerySpecifications().GetMaxSomething(this); } } public class ProductItemQuerySpecifications() { public int GetMaxSomething(product) { var respository = MyContainer.Resolve(); return respository.GetMaxSomething(product); } } 

如何获得对存储库的引用是您的选择(DI,服务定位器等)。 虽然这消除了Entity和Respository之间的直接引用,但它并没有减少LoC。

一般来说,如果我知道GetXXX()方法的数量将来会引起问题,我只会提前介绍它。 否则,我将把它留给将来的重构练习。

我相信DDD,无论何时遇到这样的问题, 你都应该先问问自己你的实体是否设计得当

如果您说该产品有一个项目列表。 您说项目是产品聚合的一部分。 这意味着,如果您对产品执行数据更改,您也会更改项目。 在这种情况下,您的产品及其商品必须具有交易一致性。 这意味着对一个或另一个的更改应始终级联整个Product聚合,并且更改应为ATOMIC。 这意味着,如果您更改了产品的名称及其中一个项目的名称,并且如果项目名称的数据库提交有效,但产品名称失败,则应回滚项目的名称。

这是聚合应该表示一致性边界而不是组成方便的事实

如果您的域中没有必要要求对项目进行更改并且对产品的更改在事务上保持一致,那么Product不应该保留对Items的引用。

您仍然可以建模产品和项目之间的关系 ,您不应该直接参考。 相反,您希望有一个间接引用,即Product将有一个Item ID列表。

具有直接引用和间接引用之间的选择应首先基于事务一致性问题。 一旦您回答了这个问题,如果您似乎需要事务一致性,那么您必须进一步询问它是否会导致可伸缩性和性能问题。

如果你有太多的产品太多产品,这可能会扩展和表现不佳。 在这种情况下,您应该考虑最终的一致性。 这时您仍然只有从Product到项目的间接引用,但是通过其他一些机制,您可以保证在将来某个时间点(希望尽快),Product和Items将处于一致状态。 例如,随着项目余额的变化,产品总余额增加,而每个项目逐个改变,产品可能不完全具有正确的总余额,但只要所有项目完成更改,产品将自动更新以反映新的总余额,从而返回到一致状态。

最后的选择很难做,你必须确定是否可以接受最终的一致性,以避免可伸缩性和性能问题,或者如果成本太高,你宁愿具有事务一致性并且具有可扩展性和性能问题。

现在,一旦您对Items有间接引用,您如何执行GetMaxItemSmth()?

在这种情况下,我认为最好的方法是使用双重调度模式。 您创建一个ItemProcessor类:

 public class ItemProcessor { private readonly IItemRepository _itemRepo; public ItemProcessor(IItemRepository itemRepo) { _itemRepo = itemRepo; } public Item GetMaxItemSmth(Product product) { // Here you are free to implement the logic as performant as possible, or as slowly // as you want. // Slow version //Item maxItem = _itemRepo.GetById(product.Items[0]); //for(int i = 1; i < product.Items.Length; i++) //{ // Item item = _itemRepo.GetById(product.Items[i]); // if(item > maxItem) maxItem = item; //} //Fast version Item maxItem = _itemRepo.GetMaxItemSmth(); return maxItem; } } 

它是相应的界面:

 public interface IItemProcessor { Item GetMaxItemSmth(Product product); } 

哪个将负责执行您需要的逻辑,包括使用您的产品数据和其他相关实体数据。 或者这可以托管任何类型的复杂逻辑,这些逻辑跨越多个实体并且不完全适合每个实体的任何一个实体,因为它需要跨越多个实体的数据。

在您的Product实体上添加:

 public class Product { private List _items; // indirect reference to the Items Product is associated with public List Items { get { return _items; } } public Product(List items) { _items = items; } public Item GetMaxItemSmth(IItemProcessor itemProcessor) { return itemProcessor.GetMaxItemSmth(this); } } 

注意:如果您只需查询Max项并获取值,而不是实体,则应完全绕过此方法。 创建一个具有GetMaxItemSmth的IFinder,它返回您的专用读取模型。 可以使用仅用于查询的单独模型,以及执行专门查询以检索此类专用读取模型的一组Finder类。 您必须记住,聚合只存在于数据更改的目的。 存储库仅适用于聚合。 因此,如果没有数据更改,则不需要聚合或存储库。

(免责声明,我刚刚开始掌握DDD。或者至少相信这样做:))

我将在第二部分马克第二,并强调我花了一些时间才意识到的2点。

  • 根据聚合物来考虑您的对象,这会导致

    关键是要么将子项与父项一起加载,要么单独加载它们

困难的部分是考虑手头问题的聚合,而不是集中支持它的数据库结构。
一个强调这一点的例子我是customer.Orders。 您是否真的需要客户的所有订单来添加新订单? 通常没有。 如果她有1毫升呢?
您可能需要诸如OutstandingAmount或AmountBuyedLastMonth之类的东西来实现诸如“AcceptNewOrder”或ApplyCustomerCareProgram之类的场景。

  • 产品是您的sceanrio的真正聚合根吗?

    如果产品不是聚合根,该怎么办?

即你要操纵物品或产品吗?
如果是产品,您需要ItemWithMaxSomething还是需要MaxSomethingOfItemsInProduct?

  • 另一个神话:PI意味着你不需要考虑数据库

鉴于您在场景中确实需要带有maxSomething的项目,那么您需要知道数据库操作的含义,以便通过服务或属性选择正确的实现。
例如,如果产品具有大量项目,则解决方案可能是将项目的ID与产品一起记录在db中,而不是遍历所有列表。

我在DDD中的难点在于定义正确的聚合。 我觉得越来越多,如果我需要依赖延迟加载,那么我可能已经监督了一些上下文边界。

希望这可以帮助 :)

我认为这是一个难以回答的难题。

一个答案的关键是分析聚合和关联,如域驱动设计中所讨论的。 关键是要么将子项与父项一起加载, 要么单独加载它们。

将它们与父项(示例中的Product)一起加载时,父项控制对子项的所有访问,包括检索和写入操作。 对此的一个证据是,子项必须没有存储库 – 数据访问由父级存储库管理。

所以回答你的一个问题:“为什么我在产品中需要这个系列呢?” 也许你不这样做,但如果你这样做,那就意味着在加载产品时总会加载Items。 您可以通过查看列表中的所有项目来实现Max方法,该方法只需查找Max。 这可能不是最高性能的实现,但如果Product是Aggregate Root,那将是这样做的方法。

如果产品不是聚合根,该怎么办? 好吧,首先要做的是从Product中删除Items属性。 然后,您将需要某种可以检索与产品关联的项目的服务。 这样的服务也可以有一个GetMaxItemSmth方法。

像这样的东西:

 public class ProductService { private readonly IItemRepository itemRepository; public ProductService (IItemRepository itemRepository) { this.itemRepository = itemRepository; } public IEnumerable GetMaxItemSmth(Product product) { var max = this.itemRepository.GetMaxItemSmth(product); // Do something interesting here return max; } } 

这与您的扩展方法非常接近,但存在显着差异,即存储库应该是注入服务的实例。 静态东西永远不适合建模目的。

就像它在这里一样,ProductService是一个非常薄的包装器本身,所以它可能是多余的。 然而,通常情况下,它是一个添加其他有趣行为的好地方,因为我试图用我的代码注释暗示。

解决此问题的另一种方法是在聚合根中跟踪它。 如果Product和Item都是同一聚合的一部分,而Product是根,那么对所有Items的访问都是通过Product控制的。 因此,在您的AddItem方法中,将新项目与当前最大项目进行比较,并在需要时替换它。 将其保留在Product中所需的位置,这样您就不必运行SQL查询。 这是定义聚合促进封装的一个原因。

请记住,NHibernate是数据库和对象之间的映射器 。 您的问题在我看来,您的对象模型不是一个可行的关系模型,这没关系,但您需要接受它。

为什么不将另一个集合映射到您的Product实体,该实体使用关系模型的强大function以高效的方式加载。 我是否正确地假设选择这个特殊集合的逻辑不是火箭科学并且可以很容易地在过滤的NHibernate映射集合中实现?

我知道我的答案含糊不清,但我只能概括地理解你的问题。 我的观点是,如果以面向对象的方式处理关系数据库, 就会遇到问题。 像NHibernate这样的工具可以弥合它们之间的差距,而不是以同样的方式对待它们。 请随意让我澄清一些我没有说清楚的观点。

您现在可以直接使用NHibernate 5直接执行此操作而无需特定代码! 它不会将整个集合加载到内存中。

请参阅https://github.com/nhibernate/nhibernate-core/blob/master/releasenotes.txt

 Build 5.0.0 ============================= ** Highlights ... * Entities collections can be queried with .AsQueryable() Linq extension without being fully loaded. ...