Person是一个聚合根吗?

时间:2013-09-11 14:18:52

标签: c# linq nhibernate domain-driven-design aggregate

由于所有实体都标记了谁创建/修改了记录,我们是否可以将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。

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

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

如果我们从Person到OrderHeader手动执行LEFT JOIN(即,通过Linq的join和.DefaultIfEmpty),则不能在NHibernate的Linq上使用.DefaultIfEmpty(LEFT JOIN功能)。如果我们执行手动LEFT JOIN,则.DefaultIfEmpty会抛出异常在NHibernate上,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(客户是另一个示例))都可能包含 ALL 实体的集合,因为每个表都有一个CreatedByPerson引用。

@DanielSchilling:

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

2 个答案:

答案 0 :(得分: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进行建模并离开{{ 1}}出模型。这意味着将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进行建模。这是因为此SU​​M最有效地直接访问相关的OrderItems,而不是像DDD所指示的那样首先通过orderItem.OrderHeader。我很好。

未映射关系的左外连接

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

OrderHeader

...和另一个让没有订单的人:

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

答案 1 :(得分:1)

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

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

How to avoid holding reference to non-root-aggregate in another non-root-aggregate?

DDD: one-to-many relationship between user aggregate root and almost all entities in other aggregates