具有 Entity Framework 4.1 和父/子关系的存储库模式
我对存储库模式仍然有些困惑。我想要使用此模式的主要原因是避免从域调用 EF 4.1 特定的数据访问操作。我宁愿从 IRepository 接口调用通用 CRUD 操作。这将使测试变得更容易,如果将来我必须更改数据访问框架,我将能够做到这一点,而无需重构大量代码。
以下是我的情况的示例:
我在数据库中有 3 个表:Group
、Person
和 GroupPersonMap
。 GroupPersonMap
是一个链接表,仅由 Group
和 Person
主键组成。我使用 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> 写入数据库,这将导致主键约束错误。我必须使用
DbSet
的 Attach
方法来告诉 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 to
Unchangedso the
Group` 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
不,不会让你的测试更容易< /a>. 您暴露了
IQueryable
所以你的存储库不可进行单元测试。不,无论如何,您都必须更改大量代码,因为您暴露了 IQueryable,并且因为 EF / ORM 是有漏洞的抽象 - 您的上层期望在 ORM 内部神奇地发生一些行为(例如延迟加载)。这也是使用存储库的最奇怪的原因之一。现在只需选择正确的技术并使用它即可获得收益。如果您稍后必须更改它,则意味着 您犯了一个错误并选择了错误或要求已更改 - 无论哪种情况,都将需要大量工作。
是的,因为您正在为每个存储库使用新的上下文=这是错误的方法。存储库必须共享上下文。您的第二个解决方案也不正确,因为您将 EF 依赖项放回到应用程序 - 存储库正在公开上下文。这通常通过第二种模式——工作单元来解决。 工作单元包装上下文 和工作单元形成原子更改集 - SaveChanges 必须在工作单元上公开,以提交所有相关存储库完成的更改。
为什么要改变状态?您从存储库接收了实体,因此在将其分离之前,没有理由调用
Attach
并手动更改状态。这一切都应该在附加实体上自动发生。只需调用SaveChanges
即可。如果您使用的是分离实体,则 您必须正确为每个实体和关系设置状态,因此在这种情况下,您确实需要一些逻辑或更新重载来处理所有场景。我不这么认为。首先,您没有使用聚合根。如果您这样做,您会立即发现 通用存储库 不适合这样做。聚合根的存储库对每个聚合根都有特定的方法来处理由根聚合的关系。
Group
不是Person
聚合的一部分,但GroupPersonMap
应该是,因此您的 Person 存储库应该具有特定的方法来处理从人员中添加和删除组(但不要自己创建或删除组)。 IMO通用存储库是 冗余层。No it will not make your testing easier. You exposed
IQueryable
so your repository is not unit testable.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.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.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 callSaveChanges
. 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.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 ofPerson
aggregate butGroupPersonMap
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.