我应该使用域对象中的存储库还是将域对象推回到服务层?


10

我来自交易脚本世界,现在开始研究DDD。我不确定将DDD设计与数据库持久性集成在一起的正确方法。这就是我所拥有的:

一个名为OrganisationService的服务类,其接口包含用于检索和保存Organization域对象实例的方法。组织是一个聚合根,并具有与之相关的其他数据:成员和许可证。在OrganisationService中使用EF6数据库优先DBContext来检索OrganisationDB实体以及相关的MemberDB和LicenseDB实体。当由OrganisationService检索并加载到Organization域对象中时,所有这些都转换为与其等效的域对象类。该对象看起来像这样:

public class Organisation
{
    public IList<Member> Members { get; set; }
    public IList<License> Licenses { get; set; }
}

我没有在OrganisationService中使用存储库模式...我将EF本身用作存储库,因为现在EF6似乎已使存储库成为多余。

在设计的这一点上,Organization域对象是贫乏的:它看起来像EF POCO Organization类。OrganisationService类看起来很像存储库!

现在,我需要开始添加逻辑。该逻辑包括管理组织的许可证和成员。现在,在交易脚本时代,我将向OrganisationService添加方法来处理这些操作,并调用存储库以与DB进行交互,但是对于DDD,我相信此逻辑应封装在Organization域对象本身内...

这是我不确定应该做什么的地方:作为逻辑的一部分,我需要将这些数据持久化回数据库。这是否意味着我应该在Organization域对象中使用DbContext来做到这一点?在域对象内使用存储库/ EF是否不好?如果是这样,那么这种持久性在哪里?

public class Organisation
{
    public IList<Member> Members { get; set; }
    public IList<License> Licenses { get; set; }

    public void AddLicensesToOrganisation(IList<License> licensesToAdd)
    {
        // Do validation/other stuff/

        // And now Save to the DB???
        using(var context = new EFContext())
        {
            context.Licenses.Add(...
        }

        // Add to the Licenses collection in Memory
        Licenses.AddRange(licensesToAdd);
    }
}

我是否应该只对内存中的Organization域对象进行突变,然后将其推回到OrganisationService以获得持久性?然后,我必须跟踪对象上实际发生的变化(这是EF对自己的POCO所做的事情!我有点觉得EF不仅是存储库的替代品,而且还可能是域层!)

在这里的任何指导表示赞赏。

Answers:


2

您可以采用这两种方法并使其运作良好-当然,也有优点和缺点。

实体框架绝对旨在使用您的域实体。当您的域实体和数据实体属于同一类时,它会很好地工作。如果您可以依靠EF为您跟踪更改,并context.SaveChanges()在完成事务性工作后才打电话,那就更好了。这也意味着不必两次设置验证属性,一次在域模型上一次,一次在持久性实体上进行-诸如[Required][StringLength(x)]可以在业务逻辑中进行检查,从而允许您在尝试执行之前捕获无效的数据状态。进行数据库交易并获得EntityValidationException。最后,它可以快速进行编码-您无需编写映射层或存储库,而可以直接与EF上下文一起使用。它已经是一个存储库和一个工作单元,因此额外的抽象层无法完成任何工作。

将域和持久实体组合在一起的不利之处在于,最终会产生一堆[NotMapped]散布在整个属性中的属性。通常,您将需要特定于域的属性,这些属性要么是对持久数据的仅获取过滤器,要么是稍后在业务逻辑中设置的,而不是将其持久化回数据库。有时候,您可能想以一种与数据库不太兼容的方式在域模型中表示数据-例如,当使用枚举时,Entity会将它们映射到int列上-但是也许您想映射它们以易于理解的方式显示string,因此您在检查数据库时无需查阅查询。然后,您得到一个string被映射的属性,enum属性(不是)(但获取字符串并将其映射到枚举),以及公开这两个属性的API!同样,如果要跨上下文组合复杂的类型(表),则可能会遇到mappedOtherTableId和unmapped ComplexType属性,这两种属性在类上都公开显示。对于不熟悉该项目的人来说,这可能会造成混乱,并不必要地使您的域模型膨胀。

我的业务逻辑/域越复杂,将域和持久性实体结合起来就越限制性或麻烦。对于期限很短的项目,或者没有表达复杂的业务层的项目,我认为将EF实体用于这两个目的都是合适的,并且不需要从持久性中提取域。对于需要最大程度的扩展性或需要表达非常复杂的逻辑的项目,我认为最好将二者分开并处理额外的持久性复杂性。

避免手动跟踪实体更改的麻烦的一种技巧是在域模型中存储相应的持久实体ID。这可以由您的映射图层自动填充。然后,当您需要将更改持久保存回EF时,请执行任何映射之前检索相关的持久实体。然后,当您映射更改时,EF将自动检测到它们,您可以调用context.SaveChanges()而不必手动跟踪它们。

public class OrganisationService
{
    public void PersistLicenses(IList<DomainLicenses> licenses) 
    {
        using (var context = new EFContext()) 
        {
            foreach (DomainLicense license in licenses) 
            {
                var persistedLicense = context.Licenses.Find(pl => pl.Id == license.PersistedId);
                MappingService.Map(persistedLicense, license); //Right-left mapping
                context.Update(persistedLicense);
            }
            context.SaveChanges();
        }
    }
}

将不同的方法和不同的数据复杂性结合在一起时,会出现问题吗?如果您有一些可以很好地映射到数据库的东西,则直接使用EF。如果您没有,请在使用EF之前先进行映射/抽象。
欣快2014年

@Euphoric可以在映射中处理domain和db的差异。我想不出一个用例,两次添加域(非持久性和持久性)并手动处理更改跟踪比添加特殊情况的映射要少。你能指出我一个吗?(我假设使用NHibernate,也许EF更受限制?)
Firo 2014年

非常有用,实用且内容丰富的答案。标记为答案。谢谢!
MrLane 2014年

3

我不知道EF6,但是其他ORM透明地处理此问题,因此您不必显式将许可证添加到上下文中,在保存更改时它将检测到它们。因此该方法将归结为

public void AddLicenses(IEnumerable<License> licensesToAdd)
{
    // Do validation/other stuff/
    // Add to the Licenses collection in Memory
    Licenses.AddRange(licensesToAdd);
}

及其周围的代码

var someOrg = Load(orgId);

someOrg.AddLicenses(someLicences);

SaveChanges();

我的域对象不是实体,因此将它们添加到“许可证”集合(仅是一个列表)不会削减它。问题不只是关于EF,而是实际持久性发生的地方。EF中的所有持久性必须在上下文范围内进行。问题是这属于哪里?
MrLane 2014年

2
域实体和持久实体可以/应该相同,否则您将引入另一层抽象,该抽象层在99%的时间中没有任何价值,并且丢失了更改跟踪。
菲罗2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.