首页 > 解决方案 > 实体框架添加记录两次

问题描述

这是我在堆栈上的第一篇文章,很抱歉给您带来不便或我的英语不好。我在实体框架向数据库表中添加两次SaveChanges()新记录时遇到问题。User

我创建了一个UserFormViewModel基于模式窗口表单的用户输入发布数据的方法。

这是我的UserFormViewModel

    public int Id { get; set; }

    [Required]
    [StringLength(50)]
    public string Username { get; set; }

    [Required]
    [StringLength(20)]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Required]
    [StringLength(50)]
    public string Name { get; set; }

    [Required]
    [StringLength(50)]
    public string Surname { get; set; }

    [Required]
    [StringLength(255)]
    [EmailAddress]
    public string Email { get; set; }

    public byte UserRoleId { get; set; }
    public Address Address { get; set; }

    public IEnumerable<UserRole> UserRoles { get; set; }

视图中的所有输入都正确映射到AddOrEdit操作方法参数,因此当通过方法将数据保存到数据库时问题就开始了SaveChanges()。它在表中添加了两次记录。

还尝试了数据库优先方法,但结果相同。

我假设它与表之间的关系有关,但不能指出正确的原因。

Address模型类中有街道地址的标准属性:

 Id, StreetName, StreetNumber, Zip, City 

和两个外键:

    public int? UserId { get; set; }
    public int? ShopId { get; set; }

    public virtual User User { get; set; }
    public virtual Shop Shop { get; set; }

所以User和他们的地址Shop共享Address表。

另一种方法何时UserShop设置为引用Address表都可以正常工作。

但我想了解为什么这不起作用,最终使Address能够级联UserShop删除而不是使用 LINQ 删除Address

这是AddOrEditaUser及其的操作方法Address

private readonly FurnitureStoreDbContext _context;
    
public UsersController()
{
    _context = new FurnitureStoreDbContext();
}

protected override void Dispose(bool disposing)
{
    _context.Dispose();
}

[HttpPost]
public ActionResult AddOrEdit(UserFormViewModel userForm)
{
    if (userForm.Id == 0)
    {
        var newUser = new User
                          {
                              Username = userForm.Username,
                              Password = userForm.Password,
                              Name = userForm.Name,
                              Surname = userForm.Surname,
                              Email = userForm.Email,
                              UserRoleId = userForm.UserRoleId
                          };

        _context.tblUsers.Add(newUser);

        _context.SaveChanges();

        var newAddress = new Address
            {
                StreetName = userForm.Address.StreetName,
                StreetNumber = userForm.Address.StreetNumber,
                ZipCode = userForm.Address.ZipCode,
                City = userForm.Address.City,
                UserId = newUser.Id
            };

        _context.tblStreetAddresses.Add(newAddress);
        _context.SaveChanges();

        return Json(new { success = true, message = "Saved" }, JsonRequestBehavior.AllowGet);
    }
    else
    {
        var updatedUser = new User
            {
                Username = userForm.Username,
                Password = userForm.Password,
                Name = userForm.Name,
                Surname = userForm.Surname,
                Email = userForm.Email,
                UserRoleId = userForm.UserRoleId
            };

        var updatedAddress = userForm.Address;

        _context.Entry(updatedUser).State = EntityState.Modified;
        _context.SaveChanges();

        _context.Entry(updatedAddress).State = EntityState.Modified;
        _context.SaveChanges();

        return Json(new { success = true, message = "Updated" }, JsonRequestBehavior.AllowGet);
    }
}

这是仅添加的调试日志User- 不添加Address.

Violation of UNIQUE KEY constraint 'UQ__Users__536C85E479927847'发生异常是因为我已将Username字段设置为唯一并且上下文尝试添加User记录两次。

Step into: Stepping over property 'FurnitureStore.Models.User.get_Addresses'. To step into properties or operators, go to Tools->Options->Debugging and uncheck 'Step over properties and operators (Managed only)'.
Step into: Stepping over property 'FurnitureStore.Models.User.get_Addresses'. To step into properties or operators, go to Tools->Options->Debugging and uncheck 'Step over properties and operators (Managed only)'.
Opened connection at 3/27/2021 8:55:09 PM +01:00
Opened connection at 3/27/2021 8:55:09 PM +01:00
Started transaction at 3/27/2021 8:55:09 PM +01:00
Started transaction at 3/27/2021 8:55:09 PM +01:00
INSERT [dbo].[Users]([Username], [Password], [Name], [Surname], [Email], [UserRoleId])
VALUES (@0, @1, @2, @3, @4, @5)
SELECT [Id]
FROM [dbo].[Users]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()INSERT [dbo].[Users]([Username], [Password], [Name], [Surname], [Email], [UserRoleId])
VALUES (@0, @1, @2, @3, @4, @5)
SELECT [Id]
FROM [dbo].[Users]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()

-- @0: 'Username 1' (Type = String, Size = 50)
-- @0: 'Username 1' (Type = String, Size = 50)
-- @1: 'q1w2e3r4' (Type = String, Size = 20)
-- @2: 'Name 1' (Type = String, Size = 50)
-- @3: 'Surname 1' (Type = String, Size = 50)
-- @4: 'jjee@gmail.com' (Type = String, Size = 255)
-- @5: '2' (Type = Byte, Size = 1)
-- Executing at 3/27/2021 8:55:10 PM +01:00
-- @1: 'q1w2e3r4' (Type = String, Size = 20)
-- @2: 'Name 1' (Type = String, Size = 50)
-- @3: 'Surname 1' (Type = String, Size = 50)
-- @4: 'jjee@gmail.com' (Type = String, Size = 255)
-- Completed in 7 ms with result: SqlDataReader

-- @5: '2' (Type = Byte, Size = 1)
-- Executing at 3/27/2021 8:55:10 PM +01:00
Committed transaction at 3/27/2021 8:55:10 PM +01:00
-- Failed in 26 ms with error: Violation of UNIQUE KEY constraint 'UQ__Users__536C85E479927847'. Cannot insert duplicate key in object 'dbo.Users'. The duplicate key value is (Username 1).
The statement has been terminated.

Closed connection at 3/27/2021 8:55:10 PM +01:00
Closed connection at 3/27/2021 8:55:10 PM +01:00
Exception thrown: 'System.Data.Entity.Infrastructure.DbUpdateException' in EntityFramework.dll
An exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred in EntityFramework.dll but was not handled in user code
An error occurred while updating the entries. See the inner exception for details.

The thread 0x42cc has exited with code 0 (0x0).

任何帮助表示赞赏。

谢谢 :)

标签: asp.net-mvcentity-framework

解决方案


我建议将添加和更新拆分为单独的操作,因为通过 Razor 等很容易在 UI 中配置适当的链接。

无论如何,您的问题在于您的更新逻辑。使用 EF DbContexts 时,您应该在更新时寻求检索实体。例如:

var existingUser = _context.Users
    .Include(x => x.Address)
    .Single(x => x.UserId == userForm.Id);

从这里,复制您允许编辑的用户窗体中的字段。例如,如果他们可以更改姓名、姓氏和电子邮件:

existingUser.Name = userForm.Name;
existingUser.Surname = userForm.Surname;
existingUser.Email = userForm.Email;

这种方法的好处是 EF 只会为实际更改的字段生成一条 UPDATE 语句,并且只有在至少有一个实际更改的情况下。使用该Update()方法或将实体的状态设置为Modified将导致所有列的 UPDATE 语句,即使实际上没有任何更改。

下一个问题将与参考。当使用视图模型时,实体永远不应该被传递给视图。像这样的代码通常是有问题的:

var updatedAddress = userForm.Address;
_context.Entry(updatedUser).State = EntityState.Modified;

这做出了许多危险的假设,即 userForm.Address 指向有效记录,并且 DbContext 尚未跟踪具有该 ID 的实体。

在前面从 DbContext 获取用户的示例语句中,我急切地加载了地址。这种方式不是在您的用户窗体视图模型中传递地址实体,而是为地址详细信息传递一个 POCO 视图模型,您可以根据需要使用它来编辑或插入地址:

if (existingUser.Address != null)
{
    existingUser.Address.AddressLine1 = userForm.Addres.AddressLine1;
    // ...
}
else
{
    var address = new Address
    {
        AddressLine1 = userForm.Addres.AddressLine1;
        // ...
    };
    existingUser.Address = address;
}

在处理多对多或多对一关系时(例如将用户与商店关联时),获取引用非常重要。从用户的角度来看,当关联到商店时,Shop 实体应该已经存在,因此通过 ID 从 DbContext 中获取它可以验证 ShopId 是否有效,并且还可以避免最终无意中插入重复行的情况。

最后,只调用SaveChanges()一次。这确保了对各种相关实体所做的更改仅一起发生或根本不发生。

Update可以通过使用和/w EntityState 之类的方法来更新实体,Attach但是这种方法很容易出错,并且在 DbContext 可能已经在跟踪实体时可能会导致间歇性/情景运行AttachUpdate错误具有相同 ID 的实例。如前所述,它还会导致 UPDATE SQL 语句的效率降低,并使您的系统容易受到意外篡改的影响。


推荐阅读