具有实体框架 4.1 和父/子关系的存储库模式

Posted

技术标签:

【中文标题】具有实体框架 4.1 和父/子关系的存储库模式【英文标题】:Repository Pattern with Entity Framework 4.1 and Parent/Child Relationships 【发布时间】:2011-11-02 12:23:45 【问题描述】:

我仍然对存储库模式有些困惑。我想使用此模式的主要原因是避免从域调用 EF 4.1 特定的数据访问操作。我宁愿从 IRepository 接口调用通用 CRUD 操作。这将使测试更容易,如果我将来必须更改数据访问框架,我将能够这样做而无需重构大量代码。

这是我的情况的一个例子:

我在数据库中有 3 个表:GroupPersonGroupPersonMapGroupPersonMap 是一个链接表,仅由 GroupPerson 主键组成。我用 VS 2010 设计器创建了 3 个表的 EF 模型。 EF 足够聪明地假设GroupPersonMap 是一个链接表,因此它不会在设计器中显示它。我想使用我现有的域对象而不是 EF 生成的类,所以我关闭了模型的代码生成。

我现有的匹配EF模型的类如下:

public class Group

   public int GroupId  get; set; 
   public string Name  get; set; 

   public virtual ICollection<Person> People  get; set; 


public class Person

   public int PersonId get; set; 
   public string FirstName  get; set; 

   public virtual ICollection<Group> Groups  get; set; 

我有一个像这样的通用存储库接口:

public interface IRepository<T> where T: class

    IQueryable<T> GetAll();
    T Add(T entity);
    T Update(T entity);
    void Delete(T entity);
    void Save()

还有一个通用的 EF 存储库:

public class EF4Repository<T> : IRepository<T> where T: class

    public DbContext Context  get; private set; 
    private DbSet<T> _dbSet;

    public EF4Repository(string connectionString)
    
        Context = new DbContext(connectionString);
        _dbSet = Context.Set<T>();
    

    public EF4Repository(DbContext context)
    
        Context = context;
        _dbSet = Context.Set<T>();
    

    public IQueryable<T> GetAll()
    
        // code
    

    public T Insert(T entity)
    
        // code
    

    public T Update(T entity)
    
        Context.Entry(entity).State = System.Data.EntityState.Modified;
        Context.SaveChanges();
    

    public void Delete(T entity)
    
        // code
    

    public void Save()
    
        // code
    

现在假设我只想将现有的Group 映射到现有的Person。我将不得不执行以下操作:

        EFRepository<Group> groupRepository = new EFRepository<Group>("name=connString");
        EFRepository<Person> personRepository = new EFRepository<Person>("name=connString");

        var group = groupRepository.GetAll().Where(g => g.GroupId == 5).First();
        var person = personRepository.GetAll().Where(p => p.PersonId == 2).First();

        group.People.Add(person);
        groupRepository.Update(group);

但这不起作用,因为 EF 认为Person 是新的,并且会尝试将INSERT 重新Person 到数据库中,这将导致主键约束错误。我必须使用DbSetAttach 方法告诉EF Person 已经存在于数据库中,所以只需在GroupPersonMap 表中创建GroupPerson 之间的映射。

因此,为了将Person 附加到上下文中,我现在必须将Attach 方法添加到我的IRepository:

public interface IRepository<T> where T: class

    // existing methods
    T Attach(T entity);

修复主键约束错误:

EFRepository<Group> groupRepository = new EFRepository<Group>("name=connString");
EFRepository<Person> personRepository = new EFRepository<Person>(groupRepository.Context);

var group = groupRepository.GetAll().Where(g => g.GroupId == 5).First();
var person = personRepository.GetAll().Where(p => p.PersonId == 2).First();

personRepository.Attach(person);
group.People.Add(person);
groupRepository.Update(group);

已修复。现在我必须处理另一个问题,每次创建组/人员映射时,Group 都会在数据库中被更新。这是因为在我的EFRepository.Update() 方法中,实体状态被显式设置为Modified'. I must set the Group's state toUnchangedso theGroup` 表没有被修改。

要解决这个问题,我必须在不更新根实体的 IRepository 或 Group 中添加某种 Update 重载,在这种情况下:

public interface IRepository<T> where T: class

    // existing methods
    T Update(T entity, bool updateRootEntity);

Update 方法的 EF4 实现如下所示:

T Update(T entity, bool updateRootEntity)

   if (updateRootEntity)
      Context.Entry(entity).State = System.Data.EntityState.Modified;
   else
      Context.Entry(entity).State = System.Data.EntityState.Unchanged;

    Context.SaveChanges();

我的问题是:我是否以正确的方式处理这个问题?当我开始使用 EF 和存储库模式时,我的存储库开始看起来以 EF 为中心。感谢您阅读这篇长文

【问题讨论】:

【参考方案1】:

我想使用这种模式的主要原因是避免调用 来自域的 EF 4.1 特定数据访问操作。我宁愿 从 IRepository 接口调用通用 CRUD 操作。这会 让测试更容易

不,will not make your testing easier。 You exposed IQueryable 所以你的存储库is not unit testable。

如果我将来必须更改数据访问框架,我 无需重构大量代码就能做到这一点。

不,您无论如何都必须更改很多代码,因为您暴露了IQueryable,并且因为 EF / ORM 是泄漏抽象 - 您的上层期望某些行为在您的 ORM 内部神奇地发生(例如延迟加载)。这也是使用存储库的最奇怪的原因之一。现在只需选择正确的技术并使用它来获得它的赌注。如果您稍后必须更改它,这意味着you did a mistake and chose the wrong one or requirements have changed - 在任何一种情况下都需要大量工作。

但这不起作用,因为 EF 认为 Person 是新的,并且会尝试 将 Person 重新插入到数据库中,这将导致主键 约束错误。

是的,因为您为每个存储库都使用了新的上下文 = 这是错误的方法。存储库必须共享上下文。您的第二个解决方案也不正确,因为您将 EF 依赖项放回应用程序 - 存储库正在公开上下文。这通常通过第二种模式 - 工作单元来解决。 Unit of work wraps the context 和工作单元构成原子更改集 - SaveChanges 必须在工作单元上公开才能提交所有相关存储库所做的更改。

现在我遇到了数据库中正在更新的组的问题 每次我想创建一个组/个人地图。

为什么要改变状态?您从存储库收到实体,因此在您分离它之前,没有理由调用 Attach 并手动更改状态。这一切都应该在附加实体上自动发生。只需致电SaveChanges。如果您使用的是分离实体,那么 you must correctly set state for every entity 和关系,所以在这种情况下,您确实需要一些逻辑或更新重载来处理所有场景。

我是否以正确的方式处理这个问题?我的存储库开始查找 当我开始使用 EF 和存储库模式时,以 EF 为中心。

我不这么认为。首先,您没有使用聚合根。如果你这样做了,你会立即发现generic repository 不适合那个。聚合根的存储库具有每个聚合根的特定方法来处理由根聚合的关系。 Group 不是 Person 聚合的一部分,但 GroupPersonMap 应该是这样,因此您的 Person 存储库应该有特定的方法来处理从 person 添加和删除组(但不是创建或删除组本身)。 Imo 通用存储库是 redundant layer。

【讨论】:

+1 表示实体框架 + 工作单元。存储库需要共享相同的 dbcontext。我敢打赌这就是我的问题。

以上是关于具有实体框架 4.1 和父/子关系的存储库模式的主要内容,如果未能解决你的问题,请参考以下文章

存储库模式和聚合根模式和实体框架

具有存储库模式的实体框架 DAL、BLL

如何在具有存储库模式的实体框架中伪造 DbContext.Entry 方法

实体框架使用 Codefirst、通用存储库、工作单元模式保存多对多关系

使用实体框架、代码优先和 CRUD 操作的存储库模式

在实体框架和 Linq to Entities 中使用规范模式和表达式