具有 Entity Framework 4.1 和父/子关系的存储库模式

发布于 2024-12-01 03:21:30 字数 4302 浏览 1 评论 0原文

我对存储库模式仍然有些困惑。我想要使​​用此模式的主要原因是避免从域调用 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 映射到现有的 。我必须执行如下操作:

        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< /code> 写入数据库,这将导致主键约束错误。我必须使用 DbSetAttach 方法来告诉 EF Person 已存在于数据库中,因此只需在 Group 之间创建一个映射GroupPersonMap 表中的Person

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

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() 方法中,实体状态显式设置为“已修改”。我必须将组的状态设置为未更改,以便Group` 表不会被修改。

要解决此问题,我必须向 IRepository 添加某种 Update 重载,该重载不会更新根实体或 Group,在本例中:

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 为中心。感谢您阅读这篇长文

I still have some confusion with the Repository Pattern. The primary reason why I want to use this pattern is to avoid calling EF 4.1 specific data access operations from the domain. I'd rather call generic CRUD operations from a IRepository interface. This will make testing easier and if I ever have to change the data access framework in the future, I will be able to do so without refactoring a lot of code.

Here is an example of my situation:

I have 3 tables in the database: Group, Person, and GroupPersonMap. GroupPersonMap is a link table and just consists of the Group and Person primary keys. I created an EF model of the 3 tables with VS 2010 designer. EF was smart enough to assume GroupPersonMap is a link table so it doesn't show it in the designer. I want to use my existing domain objects instead of EF's generated classes so I turn off code generation for the model.

My existing classes that matches the EF model are as follows:

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; }
}

I have a generic repository interface like so:

public interface IRepository<T> where T: class
{
    IQueryable<T> GetAll();
    T Add(T entity);
    T Update(T entity);
    void Delete(T entity);
    void Save()
}

and a generic EF repository:

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
    }
}

Now suppose I just want to map an existing Group to an existing Person. I would have to do something like the following:

        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);

But this doesn't work because EF thinks Person is new, and will try to re-INSERT the Person into the database which will cause a primary key constraint error. I must use DbSet's Attach method to tell EF that the Person already exists in the database so just create a map between Group and Person in the GroupPersonMap table.

So in order to attach Person to the context I must now add an Attach method to my IRepository:

public interface IRepository<T> where T: class
{
    // existing methods
    T Attach(T entity);
}

To fix the primary key constraint error:

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);

Fixed. Now I have to deal with another issue where Group is being UPDATE'd in the database every time I create a Group/Person map. This is because in my EFRepository.Update() method, the entity state is explicitly set to Modified'. I must set the Group's state toUnchangedso theGroup` table doesn't get modified.

To fix this I must add some sort of Update overload to my IRepository that does not update the root entity, or Group, in this case:

public interface IRepository<T> where T: class
{
    // existing methods
    T Update(T entity, bool updateRootEntity);
}

The EF4 implentation of the Update method would look something like this:

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();
}

My question is: Am I approaching this the right way? My Repository is starting to look EF centric as I start to work with EF and the repository pattern. Thanks for reading this long post

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

乖乖兔^ω^ 2024-12-08 03:21:30

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

不,不会让你的测试更容易< /a>. 您暴露了IQueryable 所以你的存储库不可进行单元测试

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

不,无论如何,您都必须更改大量代码,因为您暴露了 IQueryable,并且因为 EF / ORM 是有漏洞的抽象 - 您的上层期望在 ORM 内部神奇地发生一些行为(例如延迟加载)。这也是使用存储库的最奇怪的原因之一。现在只需选择正确的技术并使用它即可获得收益。如果您稍后必须更改它,则意味着 您犯了一个错误并选择了错误或要求已更改 - 无论哪种情况,都将需要大量工作。

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

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

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

为什么要改变状态?您从存储库接收了实体,因此在将其分离之前,没有理由调用 Attach 并手动更改状态。这一切都应该在附加实体上自动发生。只需调用SaveChanges即可。如果您使用的是分离实体,则 您必须正确为每个实体和关系设置状态,因此在这种情况下,您确实需要一些逻辑或更新重载来处理所有场景。

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

我不这么认为。首先,您没有使用聚合根。如果您这样做,您会立即发现 通用存储库 不适合这样做。聚合根的存储库对每个聚合根都有特定的方法来处理由根聚合的关系。 Group 不是 Person 聚合的一部分,但 GroupPersonMap 应该是,因此您的 Person 存储库应该具有特定的方法来处理从人员中添加和删除组(但不要自己创建或删除组)。 IMO通用存储库是 冗余层

The primary reason why I want to use this pattern is to avoid calling
EF 4.1 specific data access operations from the domain. I'd rather
call generic CRUD operations from a IRepository interface. This will
make testing easier

No it will not make your testing easier. You exposed IQueryable so your repository is not unit testable.

if I ever have to change the data access framework in the future, I
will be able to do so without refactoring a lot of code.

No you will have to change a lot of code anyway because you exposed IQueryable and because EF / ORM is leaky abstraction - your upper layer expects some behavior happens magically inside your ORM (for example lazy loading). Also this is one of the most odd reasons to go for repository. Simply choose the right technology now and use it to get the bets of it. If you have to change it later it means either that you did a mistake and chose the wrong one or requirements have changed - in either case it will be a lot of work.

But this doesn't work because EF thinks Person is new, and will try to
re-INSERT the Person into the database which will cause a primary key
constraint error.

Yes because you are using a new context for each repository = that is wrong approach. Repositories must share the context. Your second solution is not correct as well because you put your EF dependency back to the application - repository is exposing the context. This is usually solved by second pattern - unit of work. Unit of work wraps the context and unit of work forms the atomic change set - SaveChanges must be exposed on unit of work to commit changes done by all related repositories.

Now I have an issue with the Group being UPDATE'd in the database
every time I want to create a Group/Person map.

Why do you change the state? You received entity from the repository so until you detached it there is no reason to call Attach and change the state manually. This all should happen automatically on attached entity. Simply call SaveChanges. If you are using detached entities then you must correctly set state for every entity and relation so in such case you will indeed needs some logic or update overloads to handle all scenarios.

Am I approaching this the right way? My Repository is starting to look
EF centric as I start to work with EF and the repository pattern.

I don't think so. First of all you are not using aggregate roots. If you do you would immediately found that generic repository is not suitable for that. Repository for aggregate roots have specific methods per aggregate root to handle working with relations aggregated by the root. Group is not part of Person aggregate but GroupPersonMap should be so your Person repository should have specific methods to handle adding and removing groups from person (but not to create or delete groups themselves). Imo generic repository is redundant layer.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文