(此文章同时发表在本人微信公众号“dotNET每日精华文章”,欢迎右边二维码来关注。)
题记:最近在ABP项目中尝试纯粹的DDD,然后遇到EF实现的Repository似乎不能很好支持DDD,但……可能是我们搞错了。
ABP即ASP.NET Boilerplate,一个融合了很多最佳实践(比如领域驱动设计,Domain Driven Design,DDD)的应用程序开发框架。当然,ABP并没有强制要求你严格使用DDD中的概念来开发。换句话说,也可以很好的支持DDD的概念,比如仓储(Repository)的概念(ABP分别提供了Entity Framework和NHibernate的实现)。
ABP在刚刚更新的0.13.0版本中引入了名为AggregateRoot的基类,可以让你方便的引入DDD中的聚合和聚合根的概念了。
不过我在基于聚合来实现功能的时候,遇到了一个问题:在EF的Repository实现下,聚合中的实体无法通过聚合根来正确删除。举例来说,假设聚合中包含Order和OrderLine两个实体,Order是聚合根,那么如果通过
Order.OrderLines.Remove(orderLine)
这样的代码来删除OrderLine数据的话,就会报错。类似的问题很多人其实也遇到过,比如这篇文章就详细解释了这种情况:http://blog.raffaeu.com/archive/2014/10/04/entity-framework-6-and-collections-with-ddd.aspx。当然在stackoverflow上也有很多这样的提问。并且这个问题在2010年就被人在UserVoice提出来了,而微软一直都没有进行改变。
一开始,我也认为这是EF存在的缺陷。但是当我再次回顾聚合和聚合根中的相关特点的时候(以下文字引用于汤雪华的博文):
每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体;
聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持 对它的引用的唯一元素;
聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;
聚合根负责与外部其他对象打交道并维护自己内部的业务规则;
基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象;
聚合内部的对象可以保持对其他聚合根的引用;
删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;
同时也找出了一个被我们常常忽视的EF特点:标识和非标识关系的区别。所谓标识关系就是:主实体(即Order)的主键除了作为依赖实体(即OrderLine)的外键也要成为依赖实体的主键的一部分(即需要混合主键)。非标识关系就是:主实体的主键只作为依赖实体的外键。两种关系的最大不同就在于:标识关系下,依赖实体不能独立于主实体存在,删除主实体也会删除依赖实体(效果类似于级联删除),删除关系就会删除依赖实体。对于这一特点的详细说明可见此MSDN文档的"Considerations for Identifying and Non-identifying Relationships”。
结合DDD聚合的特点“聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可”,以及上面提到的EF标识关系的特点“删除关系就会删除依赖实体”。我们可能错怪EF了,正确做法是:应该把聚合中的非根实体设置为标识关系。这样的建模方式即体现了OrderLine的ID是本地标识的特性(要全局唯一必须混合Order的ID),又满足了通过聚合根来删除明细实体的目的。
其实这是一个很细的设计和实现问题,很多人有遇到,当然在stackoverflow也有人给出了解决方案。我这里只是给出了一点自己的思考,意欲解释一下这个问题出现的根源和解决方案背后的理论基础。