为什么我的 Attach() 方法会触发 "Entity already exists" InvalidOperationException?
Why does my Attach() method trigger a "Entity already exists" InvalidOperationException?
这让我困惑了几个小时。我正在重写 Winforms 桌面应用程序以支持 ASP.NET 核心网站。该应用程序将一些表本地存储在 LiteDB 缓存中,并调用“使用”DBContext 来获取数据。
桌面应用程序使用 TaxAccount
抽象 class,由 Household
和 Business
继承。
在客户端搜索中,应用程序调用 GetAccount() 以显示单个用户帐户。由于数据库可能很慢,缓存在后台更新。方法在这里。
/// <summary>
/// Retrieve a single account from cache. Later, replace the account object with object from server.
/// </summary>
/// <param name="accountID"></param>
/// <returns></returns>
public TaxAccount GetAccount(int accountID)
{
var accounts = Cache.GetCollection<TaxAccount>();
var account = accounts.FindById(accountID);
if (GetSingleAccountTask == null || GetSingleAccountTask.IsCompleted)
{
GetSingleAccountTask = Task.Run(() => UpdateAccount(account));
}
return account;
void UpdateAccount(TaxAccount account)
{
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
var found = serverContext.Accounts
.Include(X => X.Users)
.FirstOrDefault(X => X.Id == account.Id);
account = found;
if (found != null)
{
accounts.Update(found);
}
else
{
accounts.Delete(account.Id);
}
}
}
}
我想更新 TaxAccount
实体的单个属性。为此,我使用 Attach(taxAccount)
,理想情况下应该只更新我想要的 属性。
public void UpdatePrivateLink(TaxAccount taxAccount, string link)
{
// Retrieve collection from cache.
var accounts = Cache.GetCollection<TaxAccount>();
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
// Attach taxAccount to server context.
serverContext.Attach(taxAccount);
taxAccount.PrivateFolderLink = link;
// Update server.
serverContext.SaveChanges();
// Update cache.
accounts.Update(taxAccount);
}
}
这行不通。它创建了一个 System.InvalidOperationException
: The instance of entity type 'Household' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked
。但我找不到实体。
这是我尝试过的事情的列表:
- 将 Get() 查询更改为 .AsNoTracking() 没有任何作用。
- serverContext.ChangeTracker.Clear() 什么都不做。
- serverContext.Entry(taxAccount) returns EntityState.Detached
州
- serverContext.ChangeTracker.ToDebugString()
中没有元数据
- serverContext.Find(taxAccount.Id) 命中数据库
- 使用 accounts.FindbyId(taxAccount.Id) 直接从 LiteDB 缓存中检索会产生相同的错误。
更糟糕的是,如果我创建一个具有相同 ID 的 new Household()
,那么突然之间它就可以工作了!
var account = new Household() { Id = taxAccount.Id };
serverContext.Attach(account);
account.PrivateFolderLink = link;
serverContext.SaveChanges();
// Then we have to save in cache.
taxAccount.PrivateFolderLink = link;
accounts.Update(taxAccount);
这个解决方法对我来说毫无意义。为什么 EF 认为 taxAccount
是在全新的 DbContext 上跟踪的?为什么我不能在不创建新对象的情况下摆脱这种跟踪?
希望得到建议。
编辑:
- serverContext.Accounts.Local 不包含任何元素。
编辑:
此测试是最简单的实现,但仍然失败。
public void AttachTest(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
serverContext.Attach(acct);
acct.PrivateFolderLink = link;
serverContext.SaveChanges();
}
}
对于完整调试:我正在 .NET 5.0 控制台应用程序上进行测试,EF 版本是 5.0.13,托管在 .NET Standard 2.1 库上。
这是我正在使用的 TaxAccount 模型。
public abstract class TaxAccount
{
[Key]
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; }
public bool Archived { get; set; } = false;
public string PrivateFolderLink { get; set; }
public List<AppUser> Users { get; set; }
}
public class Household : TaxAccount
{
}
public class Business : TaxAccount
{
[EmailAddress, MaxLength(500)]
public string Email { get; set; }
[MaxLength(500)]
public string Phone { get; set; }
[MaxLength(1000)]
public string Address { get; set; }
}
在我的 ApplicationDbContext 中,唯一的 fluent
逻辑是标记鉴别器。
// Tax Account abstract class.
builder.Entity<TaxAccount>().HasDiscriminator()
.HasValue<Household>(nameof(Household))
.HasValue<Business>(nameof(Business))
.IsComplete(true);
builder.Entity<TaxAccount>()
.Property("Discriminator")
.HasMaxLength(50);
来自Microsoft Wiki:
Begins tracking the given entity and entries reachable from the given entity using >the Modified state by default, but see below for cases when a different state will >be used.
Generally, no database interaction will be performed until SaveChanges() is called.
我的猜测是,在保存更改之前跟踪帐户。
经过今天早上的一些试验,我明白了!幸运的是,它与缓存或其他 DbContexts 无关!
classTaxAccount
有一个List<AppUser>
,有一个属性Accounts
,有一个List<TaxAccount>
。这种多对多关系在 Attach()
方法中创建了一个循环,而 EF Core 无法很好地处理它。为了证明这一点,我写了两个测试,都有效!
public void AttachTest(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
// We set the Users relation to be null.
acct.Users = null;
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
serverContext.Attach(acct);
acct.PrivateFolderLink = link;
serverContext.SaveChanges();
}
}
public void AttachTestNullUsers(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
// For each user, the Accounts is null, this also breaks the relationship.
acct.Users.ForEach(X => X.Accounts = null);
using (var serverContext ...
}
现在,有两个后续问题:
- 这比创建
TaxAccount
的新实例并附加它更好吗?
- 如何处理多对多关系的
INSERT
和 UPDATE
操作?
可能不会。设置 acct.Users = null
是一个意外的结果,一旦命令完成,很容易忘记恢复该关系。 OTOH,初始化 new TaxAccount(taxAccount.Id)
是一个轻操作,对基础对象没有影响。
很糟糕,经过一些测试,如果您想添加或删除多对多对象,Attach()
不是一个好主意。查找然后更新是您最好的选择。
这让我困惑了几个小时。我正在重写 Winforms 桌面应用程序以支持 ASP.NET 核心网站。该应用程序将一些表本地存储在 LiteDB 缓存中,并调用“使用”DBContext 来获取数据。
桌面应用程序使用 TaxAccount
抽象 class,由 Household
和 Business
继承。
在客户端搜索中,应用程序调用 GetAccount() 以显示单个用户帐户。由于数据库可能很慢,缓存在后台更新。方法在这里。
/// <summary>
/// Retrieve a single account from cache. Later, replace the account object with object from server.
/// </summary>
/// <param name="accountID"></param>
/// <returns></returns>
public TaxAccount GetAccount(int accountID)
{
var accounts = Cache.GetCollection<TaxAccount>();
var account = accounts.FindById(accountID);
if (GetSingleAccountTask == null || GetSingleAccountTask.IsCompleted)
{
GetSingleAccountTask = Task.Run(() => UpdateAccount(account));
}
return account;
void UpdateAccount(TaxAccount account)
{
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
var found = serverContext.Accounts
.Include(X => X.Users)
.FirstOrDefault(X => X.Id == account.Id);
account = found;
if (found != null)
{
accounts.Update(found);
}
else
{
accounts.Delete(account.Id);
}
}
}
}
我想更新 TaxAccount
实体的单个属性。为此,我使用 Attach(taxAccount)
,理想情况下应该只更新我想要的 属性。
public void UpdatePrivateLink(TaxAccount taxAccount, string link)
{
// Retrieve collection from cache.
var accounts = Cache.GetCollection<TaxAccount>();
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
// Attach taxAccount to server context.
serverContext.Attach(taxAccount);
taxAccount.PrivateFolderLink = link;
// Update server.
serverContext.SaveChanges();
// Update cache.
accounts.Update(taxAccount);
}
}
这行不通。它创建了一个 System.InvalidOperationException
: The instance of entity type 'Household' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked
。但我找不到实体。
这是我尝试过的事情的列表:
- 将 Get() 查询更改为 .AsNoTracking() 没有任何作用。
- serverContext.ChangeTracker.Clear() 什么都不做。
- serverContext.Entry(taxAccount) returns EntityState.Detached 州
- serverContext.ChangeTracker.ToDebugString() 中没有元数据
- serverContext.Find(taxAccount.Id) 命中数据库
- 使用 accounts.FindbyId(taxAccount.Id) 直接从 LiteDB 缓存中检索会产生相同的错误。
更糟糕的是,如果我创建一个具有相同 ID 的 new Household()
,那么突然之间它就可以工作了!
var account = new Household() { Id = taxAccount.Id };
serverContext.Attach(account);
account.PrivateFolderLink = link;
serverContext.SaveChanges();
// Then we have to save in cache.
taxAccount.PrivateFolderLink = link;
accounts.Update(taxAccount);
这个解决方法对我来说毫无意义。为什么 EF 认为 taxAccount
是在全新的 DbContext 上跟踪的?为什么我不能在不创建新对象的情况下摆脱这种跟踪?
希望得到建议。
编辑:
- serverContext.Accounts.Local 不包含任何元素。
编辑: 此测试是最简单的实现,但仍然失败。
public void AttachTest(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
serverContext.Attach(acct);
acct.PrivateFolderLink = link;
serverContext.SaveChanges();
}
}
对于完整调试:我正在 .NET 5.0 控制台应用程序上进行测试,EF 版本是 5.0.13,托管在 .NET Standard 2.1 库上。
这是我正在使用的 TaxAccount 模型。
public abstract class TaxAccount
{
[Key]
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; }
public bool Archived { get; set; } = false;
public string PrivateFolderLink { get; set; }
public List<AppUser> Users { get; set; }
}
public class Household : TaxAccount
{
}
public class Business : TaxAccount
{
[EmailAddress, MaxLength(500)]
public string Email { get; set; }
[MaxLength(500)]
public string Phone { get; set; }
[MaxLength(1000)]
public string Address { get; set; }
}
在我的 ApplicationDbContext 中,唯一的 fluent
逻辑是标记鉴别器。
// Tax Account abstract class.
builder.Entity<TaxAccount>().HasDiscriminator()
.HasValue<Household>(nameof(Household))
.HasValue<Business>(nameof(Business))
.IsComplete(true);
builder.Entity<TaxAccount>()
.Property("Discriminator")
.HasMaxLength(50);
来自Microsoft Wiki:
Begins tracking the given entity and entries reachable from the given entity using >the Modified state by default, but see below for cases when a different state will >be used.
Generally, no database interaction will be performed until SaveChanges() is called.
我的猜测是,在保存更改之前跟踪帐户。
经过今天早上的一些试验,我明白了!幸运的是,它与缓存或其他 DbContexts 无关!
classTaxAccount
有一个List<AppUser>
,有一个属性Accounts
,有一个List<TaxAccount>
。这种多对多关系在 Attach()
方法中创建了一个循环,而 EF Core 无法很好地处理它。为了证明这一点,我写了两个测试,都有效!
public void AttachTest(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
// We set the Users relation to be null.
acct.Users = null;
using (var serverContext = new ApplicationDbContext(ServerOptions))
{
serverContext.Attach(acct);
acct.PrivateFolderLink = link;
serverContext.SaveChanges();
}
}
public void AttachTestNullUsers(int accountID, string link)
{
var accounts = Cache.GetCollection<TaxAccount>();
var acct = accounts.FindById(accountID);
// For each user, the Accounts is null, this also breaks the relationship.
acct.Users.ForEach(X => X.Accounts = null);
using (var serverContext ...
}
现在,有两个后续问题:
- 这比创建
TaxAccount
的新实例并附加它更好吗? - 如何处理多对多关系的
INSERT
和UPDATE
操作?
可能不会。设置
acct.Users = null
是一个意外的结果,一旦命令完成,很容易忘记恢复该关系。 OTOH,初始化new TaxAccount(taxAccount.Id)
是一个轻操作,对基础对象没有影响。很糟糕,经过一些测试,如果您想添加或删除多对多对象,
Attach()
不是一个好主意。查找然后更新是您最好的选择。