【问题标题】:How can I delete a related middle entity record without triggering a ConcurrencyException?如何在不触发 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 Registration{ID = RegistrationID};
        _context.Registrations.Remove(registration).State = EntityState.Deleted;
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(UserAssignments), new{ID = 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 JobIsComplete{get;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 UserContactEmail{get;set;}      
        
        // [Required(AllowEmptyStrings = true)]
        [Display(Name = "Phone Number")]
        [Phone()]
        [Column("UserPhoneNumber")]
        public string UserPhoneNumber{get;set;}
        
        [StringLength(37,ErrorMessage = "Address cannot be longer than 37 characters.")]
        [DataType(DataType.Text)]
        [Display(Name = "Address")]
        [Column("UserAddress")]
        public string UserAddress{get;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。否则你必须加载实体并删除它

标签: c# sql-server asp.net-core datatables entity-framework-core


【解决方案1】:

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

Registration registration = new Registration{ID = 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,它的阻塞对应物ExecuteSqlRawExecuteSqlInterpolatedAsync 与阻塞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), new{ID = 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), new{ID = 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';
    }
        }
    ]

【讨论】:

    猜你喜欢
    • 2021-04-04
    • 2015-09-26
    • 1970-01-01
    • 1970-01-01
    • 2018-02-19
    • 1970-01-01
    • 2015-04-05
    • 2019-10-02
    • 1970-01-01
    相关资源
    最近更新 更多