3

由于所有实体都有创建/修改记录的标记,我们可以将 Person 实体视为所有实体的聚合根吗?

也就是说,所有引用 Person 的实体都将成为 Person 的集合,例如

public class Person
{
    public virtual int PersonId { get; set; }
    public virtual string Lastname { get; set; }
    public virtual IList<OrderHeader> CreatedOrders { get; set; }
    public virtual IList<OrderHeader> ModifiedOrders { get; set; }

    // Other entities that have a reference on Person will be mapped as a collection under 
    // the Person entity
}


public class OrderHeader
{
    public virtual int OrderId { get; set; }        
    public virtual DateTime OrderDate { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual string CommentsOnThisOrder { get; set; }

    // stamp audit-level concerns
    public virtual Person CreatedBy { get; set; }
    public virtual DateTime DateCreated { get; set; }

    public virtual Person ModifiedBy { get; set; }
    public virtual DateTime DateModified { get; set; }

    public virtual IList<OrderItem> OrderItems { get; set; }
}

public class OrderItem 
{
    public virtual OrderHeader OrderHeader { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal Price { get; set; }
}

这基本上会使所有实体成为 Person 的集合,这违反了 DDD 聚合根规则。

在我对 DDD 聚合的有限理解中,OrderHeader 不能成为 Person 的集合,因为我们不应该通过 Person 保存 Order 聚合。保存 Order 聚合(对象图)的唯一*entry*点必须从 OrderHeader 完成,而不是从 Person 完成。

现在我的真正目标来了,即使它看起来很脏,我仍然希望 Order 成为 Person 的集合:

如果 OrderHeader 未作为集合映射到 Person,则有一个 ORM *cough* NHibernate *cough* 无法从 Person 到 OrderHeader 执行 LEFT JOIN (.DefaultIfEmpty)。实现从 Person 到 OrderHeader 的 LEFT JOIN 的唯一方法是将 OrderHeader 作为集合映射到 Person。

我是否应该允许基础设施问题(例如,通过 Linq 的 .DefaultIfEmpty 通过使 OrderHeader 成为 Person 的集合来促进从 Person 到 OrderHeader 的 LEFT JOIN)打破关于何时应该只有实体必须成为聚合根的规则?

如果我们不将 OrderHeader 作为一个集合映射到 Person,那么如果我们需要在 NHibernate 上从 Person 到 OrderHeader 进行 LEFT JOIN(扁平化结果,不是分层的,因此需要使用 LEFT JOIN),剩下的唯一选择是使用 QueryOver。QueryOver 与 Linq 相比,写起来非常繁琐。

如果我们从 Person 到 OrderHeader 手动执行 LEFT JOIN(即通过 Linq 的连接和 .DefaultIfEmpty),则无法在 NHibernate 的 Linq 上使用 .DefaultIfEmpty(LEFT JOIN 功能),如果我们在 NHibernate 上执行手动 LEFT JOIN,.DefaultIfEmpty 会抛出异常, NHibernate 的 Linq 上的 LEFT JOIN 必须通过集合和 .DefaultIfEmpty 完成。

如果偶尔进行(根据需要),是否打破聚合根规则是一个务实的选择?例如,我是否应该将 OrderHeader 作为集合映射到 Person,以便通过 Linq 促进从 Person 到 OrderHeader 的 LEFT JOIN?

编辑

北风数据库示例。当我们需要报告所有有订单的客户,包括那些没有订单的客户时(例如,PARIS)

 CustomerID     OrderID
 OTTIK      |   10407
 OTTIK      |   10684
 OTTIK      |   10554
 PARIS      |        
 PERIC      |   10502
 PERIC      |   10474
 PERIC      |   10995
 PERIC      |   10354
 PERIC      |   11073
 PERIC      |   10322

,我们需要做一个 LEFT JOIN:

select c.CustomerID, o.OrderID
from customers c
left join orders o on c.CustomerID = o.CustomerID
order by c.CustomerID

这可以通过 Linq 的 join 和 .DefaultIfEmpty() 来完成。但是 NHibernate 的 Linq 不能对手动 Linq 连接的结果执行 .DefaultIfEmpty,它会引发异常。NHibernate 的 DefaultIfEmpty 只能应用于集合。但是我觉得将集合映射到不是聚合根的东西违反了 DDD 聚合根规则。而且这样做,所有实体,如 Person(客户是另一个例子)都可能包含所有实体的集合,因为每个表都有一个 CreatedByPerson 引用。

@DanielSchilling:

我也很惊讶(以一种好的方式),从 OrderItem 引用 OrderHeader 违反了 DDD。我们是否有某种白皮书或 Martin Fowler 的文章对此进行了阐述?我认为从子实体引用父实体不被视为基础设施问题,因此被视为 DDD。

4

2 回答 2

5

关于 DDD 决策的思考...

听起来您希望能够做到这一点:

var people = session.Query<Person>()
    .FetchMany(x => x.CreatedOrders)
    .Where(x => x.Id == personId);

你应该只是翻转查询,就像这样......

var orders = session.Query<OrderHeader>()
    .Fetch(x => x.CreatedBy)
    .Where(x => x.CreatedBy.Id == personId);

除了 DDD 纯度之外,还有更多的原因可以避免person.CreatedOrders您的模型。如果您有一个人每天、一年中的每一天、年复一年地下 10 个订单怎么办?第一个查询几乎肯定会加载比您实际需要的更多的数据。第二个查询根本不需要person.CreatedOrders集合,它让您可以更好地控制要获取的订单以及要获取的订单数量。

var orders = session.Query<OrderHeader>()
    .Fetch(x => x.CreatedBy)
    .Where(x => x.CreatedBy.Id == personId)
    .Where(x => x.OrderDate >= DateTime.Now.AddYears(-1))
    .Skip(100)
    .Take(50);

一般来说,如果你发现你违反了 DDD 原则,这通常是一个很好的指标,表明你做错了,而且你通常不必看得太远就能找到其他支持证据。

不过,每隔一段时间,你就必须稍微改变一下规则,我认为这是可以接受的。例如,我通常尝试只映射每个关系的一侧——从 DDD 的角度来看,这就是我真正需要的。当引用其他聚合根时,这通常意味着只对many-to-one侧面进行建模,而对于聚合根内的引用 - 从根到其子项,这通常意味着只对模型进行建模one-to-many并将many-to-one模型排除在外。这将意味着离开OrderItem.OrderHeader模型。

如果您需要了解特定产品的销售量,但仅限于已提交的订单,该怎么办?最简单的方法是这样的:

var quantitySold = session.Query<OrderItem>()
    .Where(x => x.Product.Id == productId && x.OrderHeader.Submitted)
    .Sum(x => x.Quantity);

所以在这种情况下,我们需要对one-to-many orderHeader.OrderItems和进行建模many-to-one orderItem.OrderHeader。这是因为这个 SUM 直接访问相关的 OrderItem 是最有效的,而不是OrderHeader像 DDD 所指示的那样首先通过。我同意。

未映射关系的左外连接

忽略问题的 DDD 部分,这个 LEFT OUTER JOIN 可以通过组合两个单独查询的结果来模拟 - 一个是为了获取订单......

var orders = session.Query<OrderHeader>()
    .Fetch(x => x.CreatedBy);

......还有另一个是为了让没有订单的人:

var peopleWithNoOrders = session.Query<Person>()
    .Where(p => !session.Query<OrderHeader>().Any(o => o.CreatedBy == p));
于 2013-09-11T16:22:07.963 回答
1

你的Person可能是一个聚合体,你的可能是一个聚合Order体。一个聚合根实例不应引用另一个实例。尽量不要考虑 ORM 或数据库。

我可以向您指出这些答案(它们可能是相关的):

如何避免在另一个非根聚合中持有对非根聚合的引用?

DDD:用户聚合根与其他聚合中几乎所有实体之间的一对多关系

于 2013-09-12T04:18:25.437 回答