【问题标题】:Problem attaching same Entity with same Primary Key to DbContext in Entity Framework Core在 Entity Framework Core 中将具有相同主键的相同实体附加到 DbContext 时出现问题
【发布时间】:2021-11-24 22:03:09
【问题描述】:

我正在开发一个 .NET Core 3.1 Razor 页面应用程序。我正在使用 Entity Framework Core 和具有通用存储库的工作单元模式。我还使用 AddScoped 来注册我的所有服务,例如UnitOfWork 和 Repositories 等,即每个 HttpRequest 在存储库之间共享一个 DbContext。

services.AddDbContextPool<MyContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("MyConnection"))
         .EnableSensitiveDataLogging());

services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IGenericRepository<Domain.List>, ListRepository>();
services.AddScoped<IGenericRepository<Domain.ListItem>, ListItemRepository>();
//etc...

我为用户编写了一些代码来更新他们的姓名和电子邮件。一旦用户输入数据,我调用一个函数 UserAlreadyExists 来验证电子邮件地址是否已经存在于数据库中。此验证码用于在应用程序上注册的新用户,也用于已经注册并正在更新其详细信息的用户。当现有用户尝试更新其详细信息时,将使用以下代码。

public IUnitOfWork UoW { get; set; }

[BindProperty]
public Domain.User UserObj { get; set; }

public IActionResult OnPost()
{
    if(ModelState.IsValid)
    {
        if(UserObj.Id > 0)
        {
           //Update user

           //Validation to ensure if email updated, it's not already in use
           if(UserAlreadyExists(UserObj))
           {
              TempData["Message"] = "Email address already exists within database.";
              return Page();
           }

           UoW.UserRepository.Update(UserObj);
           UoW.SaveChanges();
           TempData["Message"] = "User updated.";

        }
        else
        {
          //Add user code here               
        }

        return RedirectToPage("List");
    }

   return Page();
}
    

这是检查用户是否存在的代码

private bool UserAlreadyExists(Domain.User user)
{
   bool alreadyExists = true;

   var existingUser = UoW.UserRepository
            .Find(u => u.Email.Trim().ToUpper() == user.Email.Trim().ToUpper())
            .FirstOrDefault();
        
    // Existing user
    if (existingUser == null)
    {
        alreadyExists = false;
    }
    else if (user.Email == existingUser.Email)
    {
        if (user.Id == existingUser.Id)
        {
            //User updating their details, but not their email
            alreadyExists = false;
        }               
    }

   return alreadyExists;
}

当现有用户尝试更新其姓名时,我收到此错误:

无法跟踪实体类型“用户”的实例,因为已在跟踪另一个具有键值“{Id: 1}”的实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例

我了解错误,它发生在 UserAlreadyExists 函数内部。当现有用户尝试更新其姓名时,User 实体将传递到函数中,并且该实体包含主键。然后我在名为existingUser 的验证函数中创建另一个User 实体,这就是问题所在 - 我现在有两个具有相同主键值的用户实体。

我想知道是否应该在保存更改之前将 existingUser 实体从上下文中分离出来,还是有更好的方法?

感谢任何反馈。

谢谢。

【问题讨论】:

  • 如果您只查询电子邮件匹配和 ID 匹配的记录数怎么办。如果结果为 0 则返回 false,否则返回 true。
  • @Serge 这是一个奇怪的评论。工作单元和通用存储库模式已成功用于全球 Web 应用程序。在开发 .NET 和 .NET Core 应用程序时,Pluralsight 和 Microsoft 都会教授此类模式。
  • @tcode That's a strange comment. 恰恰相反,将“通用存储库”(它实际上是一个 DAO)放在像 EF Core 这样的 ORM 之上是一种臭名昭著的反模式,会导致 ton i> 的问题。就在今天,我遇到了至少 3 个由此引起的问题。一个 DbContext已经是一个工作单元,一个 DbSet已经是一个存储库。现在我们不知道你的代码是做什么的。没有附加实体的代码。我们不知道UserAlreadyExists 是什么,尽管我怀疑它会加载您要更新的实体,从而导致错误。
  • @PanagiotisKanavos 我相信我确实发布了“相关”代码和足够的文字描述。是的,我知道是什么导致了这个问题,但我正在就如何解决它征求其他意见,因为我不确定我提出的建议是最好的方法。我认为我们最好保留这个讨论,但是,阅读此内容不会造成伤害stackoverflow.blog/2018/04/26/…

标签: c# asp.net-core entity-framework-core dbcontext unit-of-work


【解决方案1】:

目前还不清楚UoWUserRepository 类的作用。该错误是因为UserAlreadyExists 加载实体但不更新它而引起的。从错误来看,UserRepository.Update 似乎正在尝试再次附加 DTO,即使已经加载了具有相同 ID 的实体。

有两种选择:

  • 仅加载 ID 而不是整个实体
  • 更新加载的对象

仅加载 ID

如果直接使用 EF Core,则可以重写 UserAlreadyExists 以使用 LINQ 检查是否存在:

private bool UserAlreadyExists(Domain.User user)
{
    var id=await _context.Users.Where(u=>u.Email==user.Email.Trim())
                         .Select(u=>u.Id)
                         .FirstOrDefault();
    return (user.Id==id);
}

仅此而已,因为 user.Id 已知为 >0,并且电子邮件已经匹配。

u.Email 不应被修改,因为这会阻止数据库服务器使用任何索引来加快搜索电子邮件的速度。在 SQL Server 中,通常使用区分大小写的排序规则,因此没有理由使用ToLower()

在那之后,无论Update 做什么都应该有效,因为没有实体被跟踪。如果使用 EF Core,我们可以这样写:

if(!UserAlreadyExists(UserObj))
{
    _context.Users.Update(UserObj);
    _context.SaveChanges();
}

更新加载的实体

Razor CRUD tutorial 展示了如何使用ControllerBase.TryUpdateModelAsync 方法更新已加载的实体。我们可以直接加载实体并更新它,而不是检查用户是否存在:

var existingUser= _context.Users.Where(u=>u.Email==UserObj.Email.Trim())
                               .FirstOrDefault();
if(existingUser==null)
{
    _context.Users.Add(UserObj);
    _context.SaveChanges();
}
else if(existingUser.Id==UserObj.Id)
{
    if (await TryUpdateModelAsync<User>(existingUser))
    {
        _context.SaveChanges();
    }
}

TryUpdateModelAsync 将使用模型属性,因此它不需要访问 UserObj 对象。

【讨论】:

    【解决方案2】:

    请记住,归根结底,实体的 db 上下文既是工作单元(请注意您的注入),也是上下文中定义的所有模型的通用存储库,因此..您实际上可以通过而无需重新包装一切都在你以后必须维护的东西上。

    对于这种情况,由于您可能没有可用的“AsNoTracking”(由于不直接使用 ef 作为您的存储库),您可以通过 2 种方式更改代码:

    1. 使用您想要的过滤器对数据库执行计数,以便您 返回逻辑“是否已有用户”而不是实际 用户行
    2. 对 DTO 对象执行选择,即对象 实际上并没有被跟踪。做一个投影,选择 您要选择的属性,并将它们映射到普通类, 特定于任务

    【讨论】:

      【解决方案3】:

      这个问题的答案是使用实体框架的.AsNoTracking

      下面的初始查询由 EF 跟踪,但没有必要这样做,该实体仅用于比较目的,因此 DbContext 不需要跟踪它。

      var existingUser = UoW.UserRepository
                  .Find(u => u.Email.Trim().ToUpper() == user.Email.Trim().ToUpper())
                  .FirstOrDefault();
      

      相反,我在我的用户存储库中创建了一个利用.AsNoTracking 功能的方法(如下):

      public User GetUserByIdNoTracking(int Id)
      {
         return context.Users.AsNoTracking().Where(u => u.Id ==Id).FirstOrDefault();
      }
      

      然后在我的UserAlreadyExists 方法中使用它。

      //This entity is not tracked by the DbContext, there is no need, used only for comparison check
      var existingUser = UserRepository
      .GetUserByEmailNoTracking(email);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-27
        相关资源
        最近更新 更多