如何在不触发 ConcurrencyException 的情况下删除相关的中间实体记录?

Posted

技术标签:

【中文标题】如何在不触发 ConcurrencyException 的情况下删除相关的中间实体记录?【英文标题】:How can I delete a related middle entity record without triggering a ConcurrencyException? 【发布时间】:2021-10-05 22:34:00 【问题描述】:

我正在尝试从 EF Core 中删除使用 C# 和 DataTables 渲染的 ASP.NET MVC 5 生成的 SQL Server DB。

起初我能够成功删除跨 3 表关系的中间相关表记录。但是在重新启动会话后,我在尝试删除时收到此错误。

> An unhandled exception occurred while processing the request.
> DbUpdateConcurrencyException: Database operation expected to affect 1
> row(s) but actually affected 0 row(s). Data may have been modified or
> deleted since entities were loaded. See
> http://go.microsoft.com/fwlink/?LinkId=527962 for information on
> understanding and handling optimistic concurrency exceptions.

这些是我正在使用的表格。我正在尝试删除注册实体中的记录。

我检查了当我跨过代码时是否在后端发生了删除,但是当我在方法 UnassignUserRegistration(int RegistrationID) 中的这一行时,结果没有发送到数据库。

await _context.SaveChangesAsync();

我现在将向您展示我的代码。如果您需要我显示任何相关代码来帮助解决问题,或者我是否在与问题无关的代码中添加了太多内容,请告诉我,谢谢。

更新:21 年 3 月 8 日为 User 和 Job 以及 TeamContext 添加了模型。

JobController.cs

    public IActionResult GetAssignedUsers()
    
         _context.Jobs.OrderByDescending(j => j.ID).FirstOrDefault();       
         var userlist = _context.Users.Where(u => u.Registrations.Any());
         return Json(userlist);
    
    
    /// <summary>
    /// Opens up the UserAssignments view page, using the
    /// currently selected JobID in memory.
    /// </summary>
    /// <param name="ID"></param>
    /// <returns>The currently selected Job in memory</returns>
    public IActionResult UserAssignments(int? ID)
    
        if (ID == null)
        
            return NotFound();
        
        var job = _context.Jobs.Find(ID);
        return View(job);
    
    
    //TO DO: Fix method.
    [HttpGet]
    public async Task<IActionResult> UnassignUserRegistration(int RegistrationID)
    
        Registration registration = new RegistrationID = RegistrationID;
        _context.Registrations.Remove(registration).State = EntityState.Deleted;
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(UserAssignments), newID = RegistrationID);
    

注册模式

using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace Pitcher.Models

    public class Registration
    
        public int ID get;set;
        public int UserID  get; set; 
        public int JobID  get; set; 
        
        [Required]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "0:yyyy-MM-dd", ApplyFormatInEditMode = true)]
        [Display(Name = "User Start Date")]
        [Column("RegistrationDate")]
        public DateTime RegistrationDate get;set;        
        public User User get;set;
        public Job Job get;set;
    

工作模式

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace Pitcher.Models

    public class Job
            
        
        public int ID  get; set; 

        [Required]
        [StringLength(20, MinimumLength = 3, ErrorMessage = "Job Title must be bettween 3 to 20 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Job Title")]
        [Column("JobTitle")]
        public string JobTitle  get; set; 

        [StringLength(200, MinimumLength = 3, ErrorMessage = "Job Description must be bettween 200 to 3 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Description")]
        [Column("JobDescription")]
        public string JobDescription  get; set; 

        [Required]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "0:yyyy-MM-dd", ApplyFormatInEditMode = true)]
        [Display(Name = " Start Date")]
        [Column("JobStartDate")]
        public DateTime JobStartDate get;set;

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "0:yyyy-MM-dd", ApplyFormatInEditMode = true)]
        [Display(Name = "Deadline Date")]
        [Column("JobDeadlineDate")]
        public DateTime JobDeadline get;set;

        [Display(Name = "Job Is Complete?")]
        [Column("JobIsComplete")]
        public bool JobIsCompleteget;set;

        public ICollection<Registration> Registrations get;set;

        public ICollection<Result> Results get;set;
       

用户模型

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Pitcher.Models;
namespace Pitcher.Models

    public class User
          
        public int ID  get; set; 

        [Required]
        [StringLength(20, MinimumLength = 2, ErrorMessage = "* First Name be bettween 2 to 20 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "First Name")]
        [Column("UserFirstName")]
        public string UserFirstName  get; set;    

        [Required]
        [StringLength(30, MinimumLength = 2, ErrorMessage = "* Last Name be bettween 2 to 30 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Last Name")]
        [Column("UserLastName")]
        public string UserLastName  get; set;         
                
        [Required]
        [StringLength(30, MinimumLength = 3, ErrorMessage = "Email address must be bettween 3 to 30 characters.")]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email")]
        [Column("UserContactEmail")]
        public string UserContactEmailget;set;      
        
        // [Required(AllowEmptyStrings = true)]
        [Display(Name = "Phone Number")]
        [Phone()]
        [Column("UserPhoneNumber")]
        public string UserPhoneNumberget;set;
        
        [StringLength(37,ErrorMessage = "Address cannot be longer than 37 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Address")]
        [Column("UserAddress")]
        public string UserAddressget;set;
        
        //This regular expression allows valid postcodes and not just USA Zip codes.        
        [Display(Name = "Post Code")]
        [Column("UserPostCode")][DataType(DataType.PostalCode)]
        public string UserPostCode  get; set; 

        [StringLength(15,ErrorMessage = "Country cannot be longer than 15 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Country")]
        [Column("UserCountry")] 
        public string UserCountry get;set;
        
        
        [Phone()]
        [Display(Name = "Mobile Number")]
        [Column("UserMobileNumber")]
        public string UserMobileNumber get;set;

        [StringLength(3,ErrorMessage = "State cannot be longer than 3 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "State")]
        [Column("UserState")]
        public string UserState get;set;           
        
        public string UserFullname => string.Format("0 1", UserFirstName, UserLastName);

        public ICollection<Registration> Registrations get;set;
    

团队上下文

using Pitcher.Models;
using Microsoft.EntityFrameworkCore;
using Pitcher.Models.TeamViewModels;
namespace Pitcher.Data

    public class TeamContext : DbContext
    
        public TeamContext(DbContextOptions<TeamContext> options) : base(options)
        
        

        public DbSet<User> Users  get; set; 
        public DbSet<Registration> Registrations get;set;
        public DbSet<Job> Jobs get;set;     

        
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        
            modelBuilder.Entity<User>().ToTable("tblUser");
            modelBuilder.Entity<Registration>().ToTable("tblRegistration");
            modelBuilder.Entity<Job>().ToTable("tblJob");
                
    

UserAssignments.cshtml 查看表格代码

<h3>Assigned Users</h3>
<table id="registeredUsersTable" style="display: none">
<thead>
        <tr>
            <th>
               @html.DisplayNameFor(model => user.UserFirstName)
            </th>        
            <th>
               @Html.DisplayNameFor(model => user.UserLastName)
            </th>
            <th>
               @Html.DisplayNameFor(model => user.UserContactEmail)
            </th>
            <th>                
            </th>
        </tr>
    </thead>
    @if(user == null)
    
         <script type="text/javascript">
            alert("Model empty");
        </script>        
    
    else
    
        <tbody></tbody>
    
</table>
    document.getElementById('registeredUsersTable').style.display = 'block';
        var id=@Model.ID 
        $('#registeredUsersTable').DataTable(
            "ajax": 
            'type': 'get',
            'data':  ID: id,
            'dataType': "json",                  
            "url": "@Url.Action("GetAssignedUsers")",
            "dataSrc": function (result) 
                return result;
                
            ,            
            "columns": [                
             "data": "userFirstName",
             "data": "userLastName",
             "data": "userContactEmail",
            
            "data": null,
            "render": function (value) 
                return  '<a href="/Jobs/UnassignUserRegistration?RegistrationID=' + value.id + '"button type="button" class="btn btn-primary btn-block">Unassign</a>';
            
                
            ]
        );

【问题讨论】:

这是一个多对多关系,而不是middle entity,修改它不会导致并发冲突。您编写的代码并没有 删除实体,它附加了一个仅包含部分数据的新对象。当 EF 尝试保存它时,它会检测到数据不匹配并假设存在并发冲突。如果您想避免加载实体,不要使用 EF。否则你必须加载实体并删除它 【参考方案1】:

代码不会删除Registration 实体,它会附加一个仅包含部分数据的新对象,并尝试将其保持在Removed 状态,从而将其删除。

Registration registration = new RegistrationID = RegistrationID;
_context.Registrations.Remove(registration).State = EntityState.Deleted;
await _context.SaveChangesAsync();

EF使用all属性值来检测数据是否被修改,显然UserIDJobIdRegistrationDate与存储的数据不匹配,所以操作失败。

不是附加一个新的空对象,而是加载当前对象并删除它:

var registration = _context.Registrations.FirstOrDefault(r=>r.ID=RegistrationID);
if (registration!=null)

    _context.Registrations.Remove(registration);
    await _context.SaveChangesAsync();

如果您不想加载实体,则必须直接执行 DELETE SQL 查询。在 EF 6 中,您可以使用 ExecuteSqlCommand :

var sql="DELETE tblRegistration where ID=@p0";
await _context.Database.ExecuteSqlCommandAsync(sql,RegistrationId);

EF 是 ORM,而不是通用数据访问库。它的工作是将对象操作转换为数据库操作。它可以在该特定实体上将Remove(someEntity) 转换为DELETE,但并不意味着直接生成DELETE。这是底层数据访问库 ADO.NET 的工作。 ExecuteSqlCommandAsync 提供了一种使用 ADO.NET 执行任意参数化 SQL 命令的方法,而无需显式构造和配置 DbCommand 对象。

EF 核心

EF Core 没有区别。在使用 ORM 本身删除实体之前,必须先加载它。

唯一改变的是用于执行直接 SQL 命令的方法的名称。它们被命名为ExecuteSqlRawAsync,它的阻塞对应物ExecuteSqlRaw 和ExecuteSqlInterpolatedAsync 与阻塞ExecuteSqlInterpolated

不同之处在于ExecuteSqlRawAsync 使用带有参数占位符的原始 SQL 字符串,而ExecuteSqlInterpolatedAsync 可以将插值字符串视为参数化 SQL 字符串:

var sql="DELETE tblRegistration where ID=@id";
await _context.Database.ExecuteSqlRawAsync(sql,id);

await _context.Database.ExecuteSqlInterpolatedAsync(
      "DELETE tblRegistration where ID=id);

在这种情况下,ExecuteSqlInterpolatedAsync("DELETE tblRegistration where ID=id) 不会生成可能包含恶意内容的 SQL 字符串,而是使用参数创建参数化查询,并将 id 值作为该参数的值传递。

Using Raw SQL queries 页面解释了如何使用参数,尤其是插值字符串,以及为什么插值字符串只能Interpolated 方法一起使用以避免 SQL 注入。太多人使用字符串插值向查询中注入值,从而引入 SQL 注入漏洞,因此创建了两个单独的方法来明确每种情况下发生的情况。

【讨论】:

thx 获取信息,使用 .net5“核心”直到支持失效,如果我使用 EF 6 则需要升级。加载当前对象并删除的代码不会编译。这里的第一行。 var registration = _context.Registrations.FirstOrDefault(r=>r.ID=RegistrationID);就在 RegistrationID 的第一个 r 到结尾期间,在第 61 列返回错误。错误说。无法将类型“int”隐式转换为“bool”[Pitcher] 无法将 lambda 表达式转换为预期的委托类型,因为块中的某些返回类型不能隐式转换为委托返回类型 [Pitcher] 也很抱歉,如果我没有说清楚,但我使用的 .NET 5 不再称为“Core”,而 Entity Framework Core 5.0.0。不是实体框架。 @JordanNash 没有区别。如果要使用 EF Core 删除实体,则必须先加载它。否则,您需要使用ExecuteSqlRawExecuteSqlInterpolated 执行原始SQL 命令【参考方案2】:

基于表结构,User和Job包含多对多关系,所以我创建了以下模型:

[Table("tblJob")]
public class Job

    [Key]
    public int ID  get; set; 
    public string JobTitle  get; set; 
    public string JobDescription  get; set; 
    public DateTime JobStartDate  get; set; 
    public DateTime JobDeadlineDate  get; set; 
    public bool JobIsComplete  get; set; 

    public List<Registration> Registrations  get; set; 

[Table("tblUser")]
public class User

    [Key]
    public int ID  get; set; 
    public String UserFirstName  get; set; 
    public string UserLastName  get; set; 
    public string UserContactEmail  get; set; 
    public string UserPhoneNumber  get; set; 
    public string UserAddress  get; set; 
    public string UserPostCode  get; set; 
    public string UserCountry  get; set; 
    public string UserMobileNumber  get; set; 
    public string UserState  get; set; 
    public List<Registration> Registrations  get; set; 


[Table("tblRegistration")]
public class Registration
 
    public int ID  get; set; 
    public int UserID  get; set; 
    public int JobID  get; set; 

    [Required]
    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "0:yyyy-MM-dd", ApplyFormatInEditMode = true)]
    [Display(Name = "User Start Date")]
    [Column("RegistrationDate")]
    public DateTime RegistrationDate  get; set; 
    public User User  get; set; 
    public Job Job  get; set; 

然后,在 ApplicationDBContext.cs 中,配置多对多关系:

public class ApplicationDbContext : IdentityDbContext

    public DbSet<Job> Jobs  get; set; 
    public new DbSet<User> Users  get; set; 
    public DbSet<Registration> Registrations  get; set; 
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    
    

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Registration>()
            .HasKey(t => t.ID);

        modelBuilder.Entity<Registration>()
            .HasOne(pt => pt.User)
            .WithMany(p => p.Registrations)
            .HasForeignKey(pt => pt.UserID);

        modelBuilder.Entity<Registration>()
            .HasOne(pt => pt.Job)
            .WithMany(t => t.Registrations)
            .HasForeignKey(pt => pt.JobID);
    

然后,添加以下测试数据:

要删除相关实体,可以使用以下代码:

var user = _context.Users.Include(c => c.Registrations).FirstOrDefault(c => c.ID == 1);

foreach (var item in user.Registrations)

    _context.Entry(item).State = EntityState.Deleted; //delete items from the Join table(registrations).

_context.Entry(user).State = EntityState.Deleted; //delete the user //
_context.SaveChanges();

然后,结果如下:

如果您不想删除用户,请尝试评论或删除此行:_context.Entry(user).State = EntityState.Deleted;

【讨论】:

您作为参考的文章在所有示例中都使用Remove。尝试将跟踪的实体视为分离有什么意义? 此外,OP 没有要求级联删除。删除注册并不意味着删除用户和工作。在 SQL 中也不会这样做 - FK 关系的方向是从 user 到注册,而不是相反。删除注册不应该删除用户 嗨@PanagiotisKanavos,谢谢你提醒我,我已经更新了我的回复。 嗨@ZhiLv。感谢您付出所有努力,但如果您问我会提供模型。和上下文。只是不确定它是否会吓跑人们帮助我解决问题。不幸的是,您的代码不适用于我的项目,因为我使用了一个更简单的上下文类,称为TeamContext.cs。同样在User.cs 中,我注意到您使用了一个列表作为注册导航属性。我使用 ICollection。 你好@ZhiLv。我的天,好像终于删除注册记录成功了!现在唯一的问题是,当我删除注册记录并使用此return RedirectToAction(nameof(UserAssignments), newID = RegistrationID); 返回它时,它将报告 NullReferenceException,因为 C# 转到方法中的最后一个花括号,并且在 UserAssignments.chstml 中的var id=@Model.ID 上报告错误。但是如果我回到浏览器上,它仍然会显示它已被删除。我现在想做的是以某种方式捕获错误,因此用户看不到它,但由于返回,我看不到。【参考方案3】:

好吧,UnassignUserRegistration 方法的问题在于我将 RegistrationID 误解为传入的实际 ID。

当我将鼠标悬停在包含该方法的取消分配按钮上时,我可以看到 URL,并且该函数需要 UserID 和 JobID 而不是 RegistrationID。

修复涉及我将 UserID 和 JobID 放入方法的参数中,然后使用 .Single 方法返回 UserID 和 JobID。

然后我只需要将 JobID 重定向到操作即可。

然后我在视图中只好返回。

UserID=' + value.id + '&JobID=' + id +'

之后没有发生错误。

JobController.cs

[HttpGet]
public async Task<IActionResult> UnassignUserRegistration(int UserID, int JobID)

    Registration registration = _context.Registrations.Single(c => c.UserID == UserID && c.JobID == JobID);     
    _context.Entry(registration).State = EntityState.Deleted; 
    await _context.SaveChangesAsync();                             
    return RedirectToAction(nameof(UserAssignments), newID = JobID);

UserAssignments.cshtml

document.getElementById('registeredUsersTable').style.display = 'block';    
$('#registeredUsersTable').DataTable(
    "ajax":                  
    "url": "@Url.Action("GetAssignedUsers")",
    "dataSrc": function (result) 
        return result;
        
    ,            
    "columns": [                
     "data": "userFirstName",
     "data": "userLastName",
     "data": "userContactEmail",
    
    "data": null,
    "render": function (value) 
        return  'Unassign';
    
        
    ]

【讨论】:

以上是关于如何在不触发 ConcurrencyException 的情况下删除相关的中间实体记录?的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL:如何在不延迟插入响应的情况下执行插入触发器?

核心数据 - 如何在不触发错误的情况下检查对象关系是不是存在

如何在不和谐中使用命令切换触发 Cog 侦听器

如何在不触发 ConcurrencyException 的情况下删除相关的中间实体记录?

如何在不覆盖现有样式的情况下向 WPF 自定义控件添加触发器?

Angular2如何在不点击的情况下触发(点击)事件