EF 4.2 Code First和DDD设计问题

13
尝试使用EF 4.2(或EF 4.1)的代码优先进行DDD开发时,我有几个问题。我做了一些广泛的研究,但没有为我的具体问题得出明确的答案。以下是我的问题:
  1. 领域无法了解持久性层,换句话说,领域与EF完全分离。但是,要将数据持久化到数据库中,每个实体必须附加到或添加到EF上下文中。我知道应该使用工厂创建聚合根的实例,因此工厂可能会将创建的实体注册到EF上下文中。这似乎违反DDD规则,因为工厂是领域的一部分,而不是持久性层的一部分。我该如何创建和注册实体,以便在需要时正确地将它们持久化到数据库中?

  2. 聚合实体是否应该创建其子实体?我的意思是,如果我有一个组织,这个组织有一组员工实体,那么组织是否应该有一个名为CreateEmployee或AddEmployee的方法?如果不是,创建员工实体应该放在哪里,记住组织聚合根“拥有”每个员工实体。

  3. 使用EF代码优先时,每个实体的ID(以数据库中的标识列形式)都自动处理,通常不应由用户代码更改。由于DDD声明领域是与持久性无关的,因此在域中公开ID似乎是一件奇怪的事情,因为这意味着域应该处理为新创建的实体分配唯一ID。我是否应该担心公开实体的ID属性?

我意识到这些问题有些开放式的设计问题,但我正在尽力坚持使用EF作为我的持久性层时遵循DDD设计模式。
提前感谢!

这些问题真不错!期待研究答案! - n8wrl
2个回答

24

关于1:我对EF不是很熟悉,但是使用基于代码/约定的映射方法,我认为将具有getter和setter的POCO映射起来并不太困难(即使将那个带有DbSet属性的 "DbContext" 类放在另一个项目中也不应该太难)。 我不认为POCO是聚合根。相反,它们代表“您想要持久化的聚合内部状态”。以下是一个示例:

// This is what gets persisted
public class TrainStationState {
  public Guid Id { get; set; }
  public string FullName { get; set; }
  public double Latitude { get; set; }
  public double Longitude { get; set; }

  // ... more state here
}

// This is what you work with
public class TrainStation : IExpose<TrainStationState> { 
  TrainStationState _state;

  public TrainStation(TrainStationState state) {
    _state = state;
    //You can also copy into member variables
    //the state that's required to make this
    //object work (think memento pattern).
    //Alternatively you could have a parameter-less
    //constructor and an explicit method
    //to restore/install state.
  }

  TrainStationState IExpose.GetState() {
    return _state;
    //Again, nothing stopping you from
    //assembling this "state object"
    //manually.
  }

  public void IncludeInRoute(TrainRoute route) {
    route.AddStation(_state.Id, _state.Latitude, _state.Longitude);
  }
}

关于聚合生命周期,主要有两种场景:

  1. 创建新的聚合: 您可以使用工厂、工厂方法、构建器、构造函数等,根据需要选择。当您需要持久化聚合时,请查询其状态并将其持久化(通常这段代码不驻留在您的域内部,而是相当通用)。
  2. 检索现有的聚合: 您可以使用存储库、DAO等,根据需要选择。重要的是要理解,从持久性存储中检索到的是一个状态POCO,您需要将其注入到原始聚合中(或使用它来填充其私有成员)。所有这些发生在存储库/DAO外观后面。不要让调用站点混淆这个通用行为。

对于2:有几件事值得一提。以下是列表:

  1. 聚合根是一致性边界。您在组织和员工之间看到了什么一致性要求?
  2. 组织可以作为员工的工厂,而不改变组织的状态。
  3. "所有权"不是聚合的相关内容。
  4. 聚合根通常具有在聚合内创建实体的方法。这是有意义的,因为根负责在聚合内强制执行一致性。

对于3:从外部分配标识符,接受它并继续。但这并不意味着要公开它们(只在状态POCO中)。


太好了!这是我正在苦苦挣扎的问题的很好的解释。在我的所有研究中,我不记得看到过这种将域对象的状态拆分出来的解决方案。我会尝试使用这个新设计,并感谢您涵盖了我所有的要点。如果可以的话,我想给您加2分。 - Alex Jorgenson
Yves,我喜欢这种方法 - 我想 - 但是你能否扩展你的代码以包括导航属性?你有关于你在那里使用的模式的更多信息链接吗? - saille
@saille 我相信其他人以前也使用过这种技术。所以我不认为这是特别的。关于导航(又称惰性加载)属性,最好不要使用。在建模聚合时,传统意义上的导航没有任何理由存在。我鼓励你去 domaindrivendesign.org 上查看 Vaughn 的有关聚合设计的白皮书。 - Yves Reynhout
1
关于贫血领域模型怎么看?你现在所做的是让 POCO 变得贫血,并将聚合转化为对它们的事务脚本... “你还可以复制到成员变量中”更糟糕。这个看似无心的评论实际上意味着:我将手动完成 Entity Framework 应该自动完成的工作。 - Bartłomiej Szypelow
我想表达的是,认识到这种技术不会变得更好,并停止自欺欺人地认为那些实体是域对象。如果你认为你可以通过EF实现持久性无知,请随意忽略这个建议。显然这不适合你。如果你认为聚合是状态对象上的事务脚本,那么在我看来,你并没有真正理解重点。虽然你可以使用EF设计松耦合模型,但很少会这样做。将状态设为私有,将基元转换为值对象/实体,在编写方便的状态下保留状态是这个过程的关键。 - Yves Reynhout
显示剩余3条评论

2
  1. EF-DDD兼容性的主要问题似乎在于如何持久化私有属性。Yves提出的解决方案似乎是某些情况下缺少EF功能的一种解决方法。例如,您无法使用要求状态属性为public的Fluent API来实现DDD。 我发现只有使用.edmx文件进行映射才能保留域实体的纯净性。它不会强制你使东西变成public或添加任何EF依赖属性。

  2. 实体应始终由某个聚合根创建。请参阅Udi Dahan的一篇很好的文章:http://www.udidahan.com/2009/06/29/dont-create-aggregate-roots/ 始终加载某个聚合并从那里创建实体还解决了将实体附加到EF上下文的问题。在这种情况下,您不需要手动附加任何内容。它将自动附加,因为从存储库加载的聚合已经附加并引用新实体。虽然存储库接口属于域,但存储库实现属于基础设施,并且知道EF、上下文、附加等。

  3. 我倾向于将自动生成的ID视为持久存储的实现细节,必须考虑域实体,但不应公开。因此,我有一个私有ID属性,它映射到自动生成的列和另一个公共ID,该ID对于域是有意义的,例如Person类的身份证ID或护照号码。如果没有这样的有意义的数据,则使用Guid类型,它具有创建(几乎)唯一标识符而无需进行数据库调用的重要功能。 因此,在此模式中,我使用那些Guid / MeaningfulID从存储库加载聚合,而自动生成的ID则由数据库在内部使用,以使连接速度更快(Guid不适用于此)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接