我在项目中使用Entity Framework 4.3.1,首先使用代码和DbContext API。我的应用程序是一个n层应用程序,其中断开连接的对象可能来自客户端。我正在使用SQL Server 2008 R2,但很快就会转向SQL Azure。我遇到了一个我似乎无法解决的问题。
想象一下我有几节课:
class A {
// Random stuff here
}
class B {
// Random stuff here
public A MyA { get; set; }
}
class C {
// Random stuff here
public A MyA { get; set; }
}
默认情况下,EF在对象图上运行。例如,如果我有一个封装A实例的B实例,并且我调用myDbSet.Add(myB);
,它也会将A的实例标记为已添加(假设它尚未被跟踪)。
我的应用程序中有一个场景,我需要明确哪些对象被持久化到数据库,而不是让它跟踪整个对象图。操作顺序如下:
A myA = new A(); // Represents something already in DB that doesn't need to be udpated.
C myC = new C() { // Represents something already in DB that DOES need to be updated.
A = myA;
}
B myB0 = new B() { // Not yet in DB.
A = myA;
}
B myB1 = new B() { // Not yet in DB.
A = myA;
}
myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;
myDbSetB.Add(myB0); // Tries to track myA with a state of Added
myDbSetB.Add(myB1);
context.SaveChanges();
此时我收到错误说AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.
我认为这是因为在myB0上调用add会将A的实例标记为Added,这与已经被跟踪的A实例冲突。
理想情况下,我可以执行类似调用myDbSet.AddOnly(myB)
的操作,但显然我们没有这个选项。
我尝试了几种解决方法:
尝试#1: 首先,我尝试创建一个帮助方法,以防止第二次添加myA。
private void MarkGraphAsUnchanged<TEntity>(TEntity entity) where TEntity : class {
DbEntityEntry entryForThis = this.context.Entry<TEntity>(entity);
IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();
foreach (DbEntityEntry entry in entriesItWantsToChange) {
if (!entryForThis.Equals(entry)) {
entry.State = System.Data.EntityState.Unchanged;
}
}
}
...
myDbSetB.Add(myB0);
MarkGraphAsUnchanged(myB0);
虽然这解决了它试图添加myA的问题,但仍然会导致ObjectStateManager中的密钥违规。
尝试#2:
我试着像上面那样做,但是将状态设置为Detached而不是Unchanged。这适用于保存,但它坚持设置myB0.A = null
,这会在我的代码中产生其他不利影响。
尝试#3:
我在整个DbContext周围使用了TransactionScope。但是,即使在每个SaveChanges()
和Attach()
之间调用Add()
,更改跟踪器也不会刷新其跟踪的条目,因此我遇到与尝试#1相同的问题。
尝试#4:
我继续使用TransactionScope,除了我使用了存储库/ DAO模式并在内部创建了一个新的DbContext并为我执行的每个不同操作调用SaveChanges()
。在这种情况下,我收到错误“存储更新,插入或删除语句影响了意外的行数。”使用SQL事件探查器时,我发现在我执行的第二次操作(第一次SaveChanges()
)上调用Add()
时,它实际上将UPDATE
SQL发送到来自第一次操作的数据库第二次 - 但不会更改任何行。这对我来说就像是一个实体框架中的错误。
尝试#5:
我没有使用TransactionScope,而是决定只使用DbTransaction。我仍然创建多个上下文,但是在创建时将预先构建的EntityConnection传递给每个新上下文(通过缓存并手动打开由第一个上下文构建的EntityConnection)。但是,当我这样做时,第二个上下文运行我已定义的初始化程序,即使它已经在应用程序首次启动时运行。在开发环境中,我为这个播种了一些测试数据,实际上它超出了对我的第一个Attach()
修改的表上的数据库锁定(但由于事务仍在打开而仍然被锁定)。
帮助!!我已经尝试了我能想到的一切,并且没有完全重构我的应用程序以不使用导航属性或使用手动构造的DAO来执行INSERT,UPDATE和DELETE语句,我很茫然。似乎必须有一种方法可以获得实体框架的O / R映射的好处,但仍然可以手动控制事务中的操作!
答案 0 :(得分:2)
您必须显示其他内容,因为您添加和添加实体的方式没有问题。以下代码会将myA
,myC
,myB0
和myB1
附加到上下文中,并将myC
的状态设置为已修改。
myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;
以下代码将正确检测所有实体已经附加,而不是抛出异常(就像在ObjectContext API中那样)或再次插入所有实体(如您所料),它只会更改myB0
和{ {1}}添加状态:
myB1
如果使用现有实体的密钥正确初始化myDbSetB.Add(myB0);
myDbSetB.Add(myB1);
和myA
,则除了单个问题外,整个代码将正确执行并保存:
myC
这看起来像independent association,独立关联有自己的状态,但设置其状态的API是not available in DbContext API。如果这是您要保存的新关系,则不会保存它,因为它仍然被跟踪为未更改。您必须使用外键关联,或者必须将上下文转换为C myC = new C() {
A = myA;
}
:
ObjectContext
并使用ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
与change state of the relation。
答案 1 :(得分:0)
正如Ladislav建议的那样,我使对象实例保持一致,这解决了它试图添加冗余As的问题。
事实证明,B0和B1实际上都封装了其他对象(分别为D0和D1),而这些对象又封装了A. D0和D1都已存在于数据库中但未被实体跟踪。
添加B0 / B1导致D0 / D1也被错误地插入。我最终使用了Ladislav建议的对象上下文API,将D0 / D1的ObjectStateEntry标记为Unchanged,并将D0 / D1和A之间的关系标记为Unchanged。这似乎做我需要的:更新C并仅插入B0 / B1。
下面是我执行此操作的代码,我在SaveChanges之前调用它。请注意,我确信仍有一些边缘情况没有得到处理,而且这不是经过彻底测试的 - 但它应该给出粗略想法需要做什么。
// Entries are put in here when they are explicitly added, modified, or deleted.
private ISet<DbEntityEntry> trackedEntries = new HashSet<DbEntityEntry>();
private void MarkGraphAsUnchanged()
{
IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();
foreach (DbEntityEntry entry in entriesItWantsToChange)
{
if (!this.trackedEntries.Contains(entry))
{
entry.State = System.Data.EntityState.Unchanged;
}
}
IEnumerable<ObjectStateEntry> allEntries =
this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
.Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
.Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified));
foreach (ObjectStateEntry entry in allEntries)
{
if (entry.IsRelationship)
{
/* We can't mark relationships are being unchanged if we are truly adding or deleting the entity.
* To determine this, we need to first lookup the entity keys, then state entries themselves.
*/
EntityKey key1 = null;
EntityKey key2 = null;
if (entry.State == EntityState.Deleted)
{
key1 = (EntityKey)entry.OriginalValues[0];
key2 = (EntityKey)entry.OriginalValues[1];
}
else if (entry.State == EntityState.Added)
{
key1 = (EntityKey)entry.CurrentValues[0];
key2 = (EntityKey)entry.CurrentValues[1];
}
ObjectStateEntry entry1 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key1);
ObjectStateEntry entry2 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key2);
if ((entry1.State != EntityState.Added) && (entry1.State != EntityState.Deleted) && (entry2.State != EntityState.Added) && (entry2.State != EntityState.Deleted))
{
entry.ChangeState(EntityState.Unchanged);
}
}
}
}
呼!!!基本模式是:
这必须“返回并清理”方法显然是次优的,但它似乎是目前最好的选择,而不必在尝试之前手动连接外围实体(例如D0 / D1)保存操作。将所有这些逻辑放在通用存储库中会有所帮助 - 逻辑只需要编写一次。我希望在将来的版本中,Entity可以直接添加此功能(并删除有关在堆上具有多个对象实例但使用相同密钥的限制)。