【问题标题】:Why does my Attach() method trigger a "Entity already exists" InvalidOperationException?为什么我的 Attach() 方法会触发“实体已存在”InvalidOperationException?
【发布时间】:2022-01-11 06:47:08
【问题描述】:

这让我难过了几个小时。我正在重写 Winforms 桌面应用程序以支持 ASP.NET Core 网站。该应用程序将一些表本地存储在 LiteDB 缓存中,并调用“使用”DBContext 来获取数据。

桌面应用使用TaxAccount抽象类,由HouseholdBusiness继承。

在客户端搜索时,应用调用 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.InvalidOperationExceptionThe 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) 返回 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);

【问题讨论】:

  • 可能是错误消息具有误导性,错误实际上意味着该实体已被跟踪,但被另一个 DbContext 跟踪。无论如何,您都应该在缓存时将其分离,因为您不希望 DbContext 停留的时间超过应有的时间。
  • 我不认为这是问题所在。默认情况下,我的所有查询都使用 .AsNoTracking() 。如果他们被跟踪,new TaxAccount() 不会也造成问题吗?
  • 在这种情况下,您能否提供一个更简单的项目/minimal reproducible example(可能在 Github 上)来重现该问题?抱歉,您提供的代码片段太难测试和诊断问题。
  • 您可以发布您的 TaxAccount 模型吗?还有你用的是哪个版本的EF?
  • 使用您的代码,只需附加并保存new Household() 即可。它必须是从缓存返回的实例中的某些内容。

标签: c# winforms entity-framework


【解决方案1】:

今天早上经过一些实验,我想通了!幸运的是,它与缓存或其他 DbContexts 无关!

TaxAccount 有一个List&lt;AppUser&gt;,它有一个属性Accounts,它是一个List&lt;TaxAccount&gt;。这种多对多关系在 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 ...
        }

现在,有两个后续问题:

  1. 这比创建TaxAccount 的新实例并附加它更好吗?
  2. 这如何处理多对多关系的INSERTUPDATE 操作?

  1. 可能不会。设置acct.Users = null 是一个意外结果,一旦命令完成,很容易忘记恢复这种关系。 OTOH,初始化new TaxAccount(taxAccount.Id) 是一个轻量级的操作,对基础对象没有影响。

  2. 很糟糕,经过一些测试,如果您想添加或删除多对多对象,Attach() 并不是一个好主意。查找然后更新是您最好的选择。

【讨论】:

    【解决方案2】:

    来自Microsoft Wiki:

    默认情况下使用 >Modified 状态开始跟踪给定实体和可从给定实体访问的条目,但请参阅下文了解将使用不同状态的情况。

    通常,在调用 SaveChanges() 之前不会执行任何数据库交互。

    我的猜测是,在保存更改之前会跟踪帐户。

    【讨论】:

    • 嘿Shu si,不幸的是这没有帮助。没有跟踪我的“获取”查询。它们要么来自缓存,要么来自 .AsNoTracking() 查询。
    • 您可以改进您的答案,添加跟踪是他们的默认行为,他们可以在Cache.GetCollection&lt;T&gt;() 中添加.AsNoTracking() 以避免被跟踪。 @RamneekSingh 考虑添加您的缓存和 accounts.FindById 方法代码...是否有 AsNoTracking
    • @Cleptus,accounts.FindById 是一种 LiteDB 方法,它不应该影响 EF Core 正在执行的操作,因为 LiteDB 是一个单独的库。
    • 请不要发布猜测作为答案。等到你可以发布 cmets。
    • 好吧,对不起。不会再发布这样的答案。
    猜你喜欢
    • 2011-02-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-10
    • 1970-01-01
    • 2013-06-20
    • 1970-01-01
    • 2010-12-08
    相关资源
    最近更新 更多