领域模型和实体框架之间的存储库模式和映射

Posted

技术标签:

【中文标题】领域模型和实体框架之间的存储库模式和映射【英文标题】:Repository pattern and mapping between domain models and Entity Framework 【发布时间】:2014-01-23 13:20:19 【问题描述】:

我的存储库处理并为富域模型提供持久性。我不想将贫血的 Entity Framework 数据实体暴露给我的业务层,因此我需要某种方式在它们之间进行映射。

在大多数情况下,从数据实体构造域模型实例需要使用参数化的构造函数和方法(因为它很丰富)。它不像属性/字段匹配那么简单。 AutoMapper 可用于相反的情况(映射到数据实体),但不能用于创建领域模型。

以下是我的存储库模式的核心。

EntityFrameworkRepository 类适用于两种泛型类型:

TDomainModel:富域模型 TEntityModel: Entity Framework 数据实体

定义了两个抽象方法:

ToDataEntity(TDomainModel):转换为数据实体(用于Add()Update() 方法) ToDomainModel(TEntityModel):构建领域模型(用于Find() 方法)。

这些方法的具体实现将定义相关存储库所需的映射。

public interface IRepository<T> where T : DomainModel

    T Find(int id);
    void Add(T item);
    void Update(T item);


public abstract class EntityFrameworkRepository<TDomainModel, TEntityModel> : IRepository<TDomainModel>
    where TDomainModel : DomainModel
    where TEntityModel : EntityModel

    public EntityFrameworkRepository(IUnitOfWork unitOfWork)
    
        // ...
    

    public virtual TDomainModel Find(int id)
    
        var entity = context.Set<TEntityModel>().Find(id);

        return ToDomainModel(entity);
    

    public virtual void Add(TDomainModel item)
    
        context.Set<TEntityModel>().Add(ToDataEntity(item));
    

    public virtual void Update(TDomainModel item)
    
        var entity = ToDataEntity(item);

        DbEntityEntry dbEntityEntry = context.Entry<TEntityModel>(entity);

        if (dbEntityEntry.State == EntityState.Detached)
        
            context.Set<TEntityModel>().Attach(entity);

            dbEntityEntry.State = EntityState.Modified;
        
    

    protected abstract TEntityModel ToDataEntity(TDomainModel domainModel);
    protected abstract TDomainModel ToDomainModel(TEntityModel dataEntity);

以下是存储库实现的基本示例:

public interface ICompanyRepository : IRepository<Company>

    // Any specific methods could be included here


public class CompanyRepository : EntityFrameworkRepository<Company, CompanyTableEntity>, ICompanyRepository

    protected CompanyTableEntity ToDataEntity(Company domainModel)
    
        return new CompanyTable()
        
            Name = domainModel.Name,
            City = domainModel.City
            IsActive = domainModel.IsActive
        ;
    

    protected Company ToDomainModel(CompanyTableEntity dataEntity) 
    
        return new Company(dataEntity.Name, dataEntity.IsActive)
        
            City = dataEntity.City
        
    

问题:

一个Company 可能由许多Departments 组成。如果我想在获取Company 时从CompanyRepository 急切地加载这些,那么我将在哪里定义DepartmentDepartmentDataEntity 之间的映射?

我可以在CompanyRepository 中提供更多映射方法,但这很快就会变得混乱。很快就会在整个系统中出现重复的映射方法。

解决上述问题的更好方法是什么?

【问题讨论】:

1.严格按照经营理念定义公司。 2. 为实体用例建模 3. 请注意,如果做得正确,您将不会遇到现在遇到的问题。并忽略与持久性相关的任何内容 AutoMapper 可以用于相反的情况[...],但在创建域模型时却不行。 为什么不可能both 方式? @ThomasWeller,AutoMapper 的创建者 Jimmmy Bogard 给出了答案 - The case for two-way mapping in AutoMapper。 @davenewza,您是决定使用问题中描述的方法还是直接映射到域模型? 【参考方案1】:

我的存储库处理并为富域模型提供持久性。我不想将贫血的 Entity Framework 数据实体暴露给我的业务层,因此我需要某种方式在它们之间进行映射。

如果您使用实体框架,它可以映射富域模型本身。

我最近回答了类似的问题"Advice on mapping of entities to domain objects"。

我一直在使用 NHibernate,并且知道在 Entity Framework 中您还可以指定从 DB 表到 POCO 对象的映射规则。在 Entity Framework 实体之上开发另一个抽象层是一项额外的工作。让 ORM 负责所有的 mappings、状态跟踪、unit of work 和 identity map 实现等。现代 ORM 知道如何处理所有这些问题。

AutoMapper 可用于相反的情况(映射到数据实体),但不能用于创建领域模型。

你完全正确。

当一个实体可以映射到另一个实体而不需要额外的依赖项(例如存储库、服务等)时,Automapper 很有用。

...我将在哪里定义DepartmentDepartmentDataEntity 之间的映射?

我会将其放入DepartmentRepository 并添加方法IList&lt;Department&gt; FindByCompany(int companyId) 以检索公司的部门。

我可以在CompanyRepository 中提供更多映射方法,但这很快就会变得混乱。很快就会在整个系统中出现重复的映射方法。

解决上述问题的更好方法是什么?

如果需要获取另一个实体的Departments 列表,则应在DepartmentRepository 中添加一个新方法,并在需要的地方简单地使用。

【讨论】:

我不愿意不同意,但我发现 EF 不能正确映射到富域模型,但最适合贫乏的 POCO 类。 EF 与其他 ORM 一样,带有许多与业务规则相冲突的限制,例如 setter 的可见性或对构造函数的要求。在为业务规则和 EF 目的创建单个实体类之后,我建议不要这样做。使用数据层模型和业务/领域层模型,并根据需要在它们之间进行映射。这会将特定于 EF(ORM) 的代码从域逻辑中分离出来,并将促进 SRP。不过没有减号。 我同意“ORM 带有许多与业务规则相冲突的限制”。尽管对于某些模型,这些限制并不重要,尤其是当您的团队纪律严明时。每个决定都应该基于上下文。【参考方案2】:

假设您有以下数据访问对象...

public class AssetDA
        
    public HistoryLogEntry GetHistoryRecord(int id)
    
        HistoryLogEntry record = new HistoryLogEntry();

        using (IUnitOfWork uow = new NHUnitOfWork())
        
            IReadOnlyRepository<HistoryLogEntry> repository = new NHRepository<HistoryLogEntry>(uow);
            record = repository.Get(id);
        

        return record;
    

它返回一个历史日志条目数据实体。该数据实体定义如下...

public class HistoryLogEntry : IEntity

    public virtual int Id
     get; set; 

    public virtual int AssetID 
     get; set; 

    public virtual DateTime Date
     get; set; 

    public virtual string Text
     get; set; 

    public virtual Guid UserID
     get; set; 

    public virtual IList<AssetHistoryDetail> Details  get; set; 

您可以看到属性Details 引用了另一个数据实体AssetHistoryDetail。现在,在我的项目中,我需要将这些数据实体映射到我的业务逻辑中使用的域模型对象。为了进行映射,我定义了扩展方法……我知道这是一种反模式,因为它是特定于语言的,但好处是它隔离并打破了彼此之间的依赖关系……是的,这就是它的美妙之处。所以,mapper的定义如下...

internal static class AssetPOMapper

    internal static HistoryEntryPO FromDataObject(this HistoryLogEntry t)
    
        return t == null ? null :
            new HistoryEntryPO()
            
                Id = t.Id,
                AssetID = t.AssetID,
                Date = t.Date,
                Text = t.Text,
                UserID = t.UserID,
                Details = t.Details.Select(x=>x.FromDataObject()).ToList()
            ;
    

    internal static AssetHistoryDetailPO FromDataObject(this AssetHistoryDetail t)
    
        return t == null ? null :
            new AssetHistoryDetailPO()
            
                Id = t.Id,
                ChangedDetail = t.ChangedDetail,
                OldValue = t.OldValue,
                NewValue = t.NewValue
            ;
    

差不多就是这样。所有依赖项都在一个地方。然后,当从业务逻辑层调用数据对象时,我会让LINQ 完成其余的工作......

var da = new AssetDA();
var entry =  da.GetHistoryRecord(1234);
var domainModelEntry = entry.FromDataObject();

请注意,您可以定义相同的内容将域模型对象映射到数据实体。

【讨论】:

我很想做这样的事情,但是我的领域模型与我的基础设施层相结合。 我认为您在这里稍微过分强调了 耦合 的想法 - 您的基础架构和域无论如何都是“耦合”的(也在您的示例代码中)。只要映射代码驻留在一个不是域模型和实体的位置,一切都很好。 @davenewza 您的域模型将如何与您的基础架构层耦合?领域模型是调用方法还是实例化基础设施层的具体对象?我还没有提出这样的建议……我说的是域模型和数据实体之间的映射 +1,扩展方法不会让你的领域层依赖于基础设施。它们可以在基础设施本身或另一个单独的模块中声明。这相当于使用驻留在基础设施层中的 Mapper 对象,只是更加简洁和特定于技术。【参考方案3】:

对于实体框架,恕我直言,从实体模型转换为其他形式的模型(域模型、值对象、视图模型等)通常是一个坏主意,反之亦然,除非仅在应用层因为您将失去很多只能通过实体对象实现的 EF 功能,例如丢失更改跟踪和丢失 LINQ 可查询。

最好在存储库层和应用程序层之间进行映射。将实体模型保存在存储库层中。

【讨论】:

【参考方案4】:

我喜欢使用定制的扩展方法来做实体和域对象之间的映射。

您可以轻松调用其他实体的扩展方法 位于包含实体内。 您可以轻松应对 通过创建 IEnumerable 扩展来收集。

一个简单的例子:

public static class LevelTypeItemMapping

    public static LevelTypeModel ToModel(this LevelTypeItem entity)
    
        if (entity == null) return new LevelTypeModel();

        return new LevelTypeModel
         
            Id = entity.Id;
            IsDataCaptureRequired = entity.IsDataCaptureRequired;
            IsSetupRequired = entity.IsSetupRequired;
            IsApprover = entity.IsApprover;
            Name = entity.Name;
        
    
    public static IEnumerable<LevelTypeModel> ToModel(this IEnumerable<LevelTypeItem> entities)
    
        if (entities== null) return new List<LevelTypeModel>();

        return (from e in entities
                select e.ToModel());
    


...你就这样使用它们.....

 using (IUnitOfWork uow = new NHUnitOfWork())
        
        IReadOnlyRepository<LevelTypeItem> repository = new NHRepository<LevelTypeItem>(uow);
        record = repository.Get(id);

        return record.ToModel();

        records = repository.GetAll(); // Return a collection from the DB
        return records.ToModel(); // Convert a collection of entities to a collection of models
        

并不完美,但很容易遵循并且很容易重复使用。

【讨论】:

【参考方案5】:

就像以前的帖子所说的那样。最好等到存储库之后进行实际映射。但我喜欢使用自动映射器。它提供了一种将对象映射到其他对象的非常简单的方法。对于某些关注点分离,您还可以在单​​独的项目中定义映射。这些映射也是通用的/基于类型的:

    您在映射中指定基本类型并定义它如何填充目标类型 在需要映射的地方,只需调用 Mapper.Map(baseTypeObject, DestinationTypeObject) Automapper 应该处理剩下的事情

如果我正确理解了您的问题,这可以解决问题。

【讨论】:

【参考方案6】:

我不喜欢手动映射,所以我使用http://valueinjecter.codeplex.com/进行映射

【讨论】:

以上是关于领域模型和实体框架之间的存储库模式和映射的主要内容,如果未能解决你的问题,请参考以下文章

从实体框架映射到自定义域模型类?

关于 MVC 的领域、模型和实体之间的区别

Rafy 领域实体框架简介

领域实体、DTO 和视图模型

实体 VS 领域模型 VS 视图模型

EF工作流程