实体框架告诉我一个对象已附加,但实际上并未附加 - 为什么?
我有一个要在数据库中更新的对象。我是 EF 的新手,但已经阅读了相当多的内容。显然我的方法是错误的,但我不明白为什么。仅供参考,整个引用的 Context 是一个 ObjectContext,它是在这段代码开始时新实例化的,并在之后立即被处理。这是我的 Update 方法 - View 是我想要在数据库中更新的对象,它有 4 个 ICollection 属性,我也希望将其更改保存到数据库中:
public void Update(View view)
{
var original = Read(view.Username, view.ViewId);
original.ViewName = view.ViewName;
ProcessChanges<CostCentre, short>(Context.CostCentres, original.CostCentres, view.CostCentres, "iFinanceEntities.CostCentres", "CostCentreId");
ProcessChanges<LedgerGroup, byte>(Context.LedgerGroups, original.LedgerGroups, view.LedgerGroups, "iFinanceEntities.LedgerGroups", "LedgerGroupId");
ProcessChanges<Division, byte>(Context.Divisions, original.Divisions, view.Divisions, "iFinanceEntities.Divisions", "DivisionId");
ProcessChanges<AnalysisCode, short>(Context.AnalysisCodes, original.AnalysisCodes, view.AnalysisCodes, "iFinanceEntities.AnalysisCodes", "AnalysisCodeId");
int test = Context.SaveChanges();
}
首先,我从数据库中获取原始数据,因为我想比较它的集合与新的系列。这应该确保添加和删除正确的子对象。我使用 ProcessChanges 方法依次比较每个集合:
private void ProcessChanges<TEntity, TKey>(ObjectSet<TEntity> contextObjects, ICollection<TEntity> originalCollection, ICollection<TEntity> changedCollection, string entitySetName, string pkColumnName)
where TEntity : class, ILookupEntity<TKey>
{
List<TKey> toAdd = changedCollection
.Select(c => c.LookupKey)
.Except(originalCollection.Select(o => o.LookupKey))
.ToList();
List<TKey> toRemove = originalCollection
.Select(o => o.LookupKey)
.Except(changedCollection.Select(c => c.LookupKey))
.ToList();
toAdd.ForEach(a =>
{
var o = changedCollection.Single(c => c.LookupKey.Equals(a));
AttachToOrGet<TEntity, TKey>(entitySetName, pkColumnName, ref o);
originalCollection.Add(o);
});
toRemove.ForEach(r =>
{
var o = originalCollection.Single(c => c.LookupKey.Equals(r));
originalCollection.Remove(o);
});
}
这会将新集合与旧集合进行比较,并确定要添加哪些对象以及要删除哪些对象。请注意,这些集合都包含实现 ILookupEntity 的对象。
我的问题发生在我调用 AttachToOrGet 的线路上。这个方法是我从 stackoverflow 上其他地方得到的。我之所以使用它,是因为在附加新的子对象时,我经常收到一条消息,指出“ObjectStateManager 中已存在具有相同键的对象”。希望当我在下面发布此方法的代码时,您会理解我对此的困惑:
public void AttachToOrGet<TEntity, TKey>(string entitySetName, string pkColumnName, ref TEntity entity)
where TEntity : class, ILookupEntity<TKey>
{
ObjectStateEntry entry;
// Track whether we need to perform an attach
bool attach = false;
if (Context.ObjectStateManager.TryGetObjectStateEntry(new EntityKey(entitySetName, pkColumnName, entity.LookupKey), out entry))
//if (Context.ObjectStateManager.TryGetObjectStateEntry(Context.CreateEntityKey(entitySetName, entity), out entry))
{
// Re-attach if necessary
attach = entry.State == EntityState.Detached;
// Get the discovered entity to the ref
entity = (TEntity)entry.Entity;
}
else
{
// Attach for the first time
attach = true;
}
if (attach)
Context.AttachTo(entitySetName, entity);
}
基本上这就是说,如果实体尚未附加,则附加它。 但是我的代码在 Context.ObjectStateManager.TryGetObjectStateEntry 行上返回 false,但在最后一行抛出异常,并显示消息“ObjectStateManager 中已存在具有相同键的对象”。 对我来说,这是是自相矛盾的。
就我而言,我正在努力实现一些非常简单的事情。编写一个存储过程需要 20 分钟的东西。一个简单的数据库更新。坦率地说,我不关心附加的内容和不附加的内容,因为我不想跟踪更改或创建代理或延迟加载或执行 EF 为我提供的任何其他操作。我只想采用一个非常简单的对象,并使用服务器之间最少的行程来更新数据库。这怎么这么复杂?请有人帮助我 - 我已经花了一整天的时间!
更新
这是我的 ILookupEntity 类:
public interface ILookupEntity<TKey>
{
TKey LookupKey { get; }
string DisplayText { get; }
}
以下是它在 CostCentre 中的实现方式:
public partial class CostCentre : IFinancialCode, ILookupEntity<short>
{
#region IFinancialCode Members
public short ID { get { return CostCentreId; } }
public string DisplayText { get { return string.Format("{0} - {1}", Code, Description); } }
#endregion
#region ILookupEntity Members
public short LookupKey
{
get { return ID; }
}
#endregion ILookupEntity Members
}
I have an object I want to update in the database. I'm new to EF but have done a fair bit of reading. Clearly my approach is wrong, but I don't understand why. FYI the Context referenced throughout is an ObjectContext which is newly instantiated as this code begins and is disposed immediately after. Here is my Update method - the View is the object I want to update in the database and it has 4 ICollection properties whose changes I also wish to save to the database:
public void Update(View view)
{
var original = Read(view.Username, view.ViewId);
original.ViewName = view.ViewName;
ProcessChanges<CostCentre, short>(Context.CostCentres, original.CostCentres, view.CostCentres, "iFinanceEntities.CostCentres", "CostCentreId");
ProcessChanges<LedgerGroup, byte>(Context.LedgerGroups, original.LedgerGroups, view.LedgerGroups, "iFinanceEntities.LedgerGroups", "LedgerGroupId");
ProcessChanges<Division, byte>(Context.Divisions, original.Divisions, view.Divisions, "iFinanceEntities.Divisions", "DivisionId");
ProcessChanges<AnalysisCode, short>(Context.AnalysisCodes, original.AnalysisCodes, view.AnalysisCodes, "iFinanceEntities.AnalysisCodes", "AnalysisCodeId");
int test = Context.SaveChanges();
}
First I get the original from the database because I want to compare its collections with the new set of collections. This should ensure the correct sub-objects are added and removed. I compare each collection in turn using this ProcessChanges method:
private void ProcessChanges<TEntity, TKey>(ObjectSet<TEntity> contextObjects, ICollection<TEntity> originalCollection, ICollection<TEntity> changedCollection, string entitySetName, string pkColumnName)
where TEntity : class, ILookupEntity<TKey>
{
List<TKey> toAdd = changedCollection
.Select(c => c.LookupKey)
.Except(originalCollection.Select(o => o.LookupKey))
.ToList();
List<TKey> toRemove = originalCollection
.Select(o => o.LookupKey)
.Except(changedCollection.Select(c => c.LookupKey))
.ToList();
toAdd.ForEach(a =>
{
var o = changedCollection.Single(c => c.LookupKey.Equals(a));
AttachToOrGet<TEntity, TKey>(entitySetName, pkColumnName, ref o);
originalCollection.Add(o);
});
toRemove.ForEach(r =>
{
var o = originalCollection.Single(c => c.LookupKey.Equals(r));
originalCollection.Remove(o);
});
}
This compares the new collection to the old one and works out which objects to add and which to remove. Note that the collections all contain objects which implement ILookupEntity.
My problems occur on the line where I call AttachToOrGet. This method I got from elsewhere on stackoverflow. I'm using this because I was often getting a message saying that "An object with the same key already exists in the ObjectStateManager" when attaching a new subobject. Hopefully you'll understand my confusion around this when I post the code of this method below:
public void AttachToOrGet<TEntity, TKey>(string entitySetName, string pkColumnName, ref TEntity entity)
where TEntity : class, ILookupEntity<TKey>
{
ObjectStateEntry entry;
// Track whether we need to perform an attach
bool attach = false;
if (Context.ObjectStateManager.TryGetObjectStateEntry(new EntityKey(entitySetName, pkColumnName, entity.LookupKey), out entry))
//if (Context.ObjectStateManager.TryGetObjectStateEntry(Context.CreateEntityKey(entitySetName, entity), out entry))
{
// Re-attach if necessary
attach = entry.State == EntityState.Detached;
// Get the discovered entity to the ref
entity = (TEntity)entry.Entity;
}
else
{
// Attach for the first time
attach = true;
}
if (attach)
Context.AttachTo(entitySetName, entity);
}
Basically this is saying if the entity is not already attached then attach it. But my code is returning false on the Context.ObjectStateManager.TryGetObjectStateEntry line, but throwing an exception on the final line with the message "An object with the same key already exists in the ObjectStateManager". To me this is paradoxical.
As far as I'm concerned I'm trying to achieve something very simple. Something it would take 20 minutes to write a stored procedure for. A simple database update. Frankly I don't care what is attached and what isn't because I don't wish to track changes or create proxies or lazy load or do anything else EF offers me. I just want to take a very simple object and update the database using a minimal number of trips between servers. How is this so complicated? Please someone help me - I've spent a whole day on this!
Update
Here's my ILookupEntity class:
public interface ILookupEntity<TKey>
{
TKey LookupKey { get; }
string DisplayText { get; }
}
Here's how it is implemented in CostCentre:
public partial class CostCentre : IFinancialCode, ILookupEntity<short>
{
#region IFinancialCode Members
public short ID { get { return CostCentreId; } }
public string DisplayText { get { return string.Format("{0} - {1}", Code, Description); } }
#endregion
#region ILookupEntity Members
public short LookupKey
{
get { return ID; }
}
#endregion ILookupEntity Members
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
好吧,我已经解决了这个问题并找到了解决方案,但我不能说我理解它。当我在@Slauma 发表评论后进行检查时,关键的因素出现了。我想检查我是否使用了正确的实体集名称等,因此我在 AttachToOrGet 方法顶部附近添加了以下几行:
奇怪的是,仅此一项就解决了问题。由于某种原因,一旦我调用了 TryGetObjectByKey,则 ObjectStateManager.TryGetObjectStateEntry 调用实际上开始查找附加实体。神奇。如果有人能解释这一点,我会很高兴。
顺便说一句,我还需要包含以下代码,但这只是因为在我的情况下,建模实体位于与上下文本身不同的程序集中。
Well, I've worked through this and found a solution, but I can't say I understand it. The crucial ingredient came when I was performing a check after the comment by @Slauma. I wanted to check I was using the correct entity set name etc so I included the following lines near the top of my AttachToOrGet method:
Bizarrely this alone resolved the problem. For some reason, once I'd called the TryGetObjectByKey then the ObjectStateManager.TryGetObjectStateEntry call actually started locating the attached entity. Miraculous. I'd love it if anyone can explain this.
By the way, I also needed to include the following code, but that's just because in my case the modelled entities are located in a separate assembly from the context itself.