如何在不触发 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属性值来检测数据是否被修改,显然UserID
、JobId
和RegistrationDate
与存储的数据不匹配,所以操作失败。
不是附加一个新的空对象,而是加载当前对象并删除它:
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 删除实体,则必须先加载它。否则,您需要使用ExecuteSqlRaw
或ExecuteSqlInterpolated
执行原始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:如何在不延迟插入响应的情况下执行插入触发器?
核心数据 - 如何在不触发错误的情况下检查对象关系是不是存在
如何在不触发 ConcurrencyException 的情况下删除相关的中间实体记录?