UPDATE 语句与 EF Core 中的 FOREIGN KEY 约束冲突
The UPDATE statement conflicted with the FOREIGN KEY constraint in EF Core
我们有 3 个模型 类:
- 主持人
- TournamentBatch
- TournamentBatchItem
Host 有很多 TournamentBatch。
TournamentBatch 有很多 TournamentBatchItem。在 TournamentBatch table 中将有 FK Host。
我们确实重写了 ApplicationDbContext 中的 SaveChangesAsync 以允许软删除,如下所示:
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
// Added
var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
added.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
((IBaseEntity)entry.Entity).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Modified
var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
modified.ForEach(entry =>
{
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
// var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted).ToList();
deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
在我们的模型中:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace AthlosifyWebArchery.Models
{
public class TournamentBatch : IBaseEntity
{
[Key]
public Guid TournamentBatchID { get; set; }
public Guid HostID { get; set; }
public string Name { get; set; }
public string BatchFilePath { get; set; }
[Display(Name = "Batch File Size (bytes)")]
[DisplayFormat(DataFormatString = "{0:N1}")]
public long BatchFileSize { get; set; }
[Display(Name = "Uploaded (UTC)")]
[DisplayFormat(DataFormatString = "{0:F}")]
public DateTime DateUploaded { get; set; }
public DateTime DateCreated { get; set; }
public string CreatedBy { get; set; }
public DateTime LastDateModified { get; set; }
public string LastModifiedBy { get; set; }
public DateTime? DateDeleted { get; set; }
public string DeletedBy { get; set; }
public bool IsDeleted { get; set; }
public Host Host { get; set; }
public ICollection<TournamentBatchItem> TournamentBatchItems { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
[ForeignKey("CreatedBy")]
public ApplicationUser ApplicationCreatedUser { get; set; }
[ForeignKey("LastModifiedBy")]
public ApplicationUser ApplicationLastModifiedUser { get; set; }
}
}
在我们的 Razorpage 中,我们有一个页面可以删除包含 TournamentBatchItem 的 TournamentBatch,方法是:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using Microsoft.Extensions.Logging;
namespace AthlosifyWebArchery.Pages.Administrators.TournamentBatches
{
public class DeleteModel : PageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly ILogger _logger;
public DeleteModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
ILogger<DeleteModel> logger)
{
_context = context;
_logger = logger;
}
[BindProperty]
public TournamentBatch TournamentBatch { get; set; }
public IList<TournamentBatchItem> tournamentBatchItems { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(Guid? id, bool? concurrencyError)
{
if (id == null)
{
return NotFound();
}
TournamentBatch = await _context.TournamentBatch
.AsNoTracking() //Addded
.FirstOrDefaultAsync(m => m.TournamentBatchID == id);
if (TournamentBatch == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(Guid? id)
{
try
{
//var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
//_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
//await _context.SaveChangesAsync();
if (await _context.TournamentBatch.AnyAsync(
m => m.TournamentBatchID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
catch(DbUpdateException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
//catch (DbUpdateConcurrencyException)
//{
// return RedirectToPage("./Delete",
// new { concurrencyError = true, id = id });
//}
}
}
}
... 我们有以下有点奇怪的错误。
System.Data.SqlClient.SqlException (0x80131904): The UPDATE statement
conflicted with the FOREIGN KEY constraint
"FK_TournamentBatch_Host_HostID". The conflict occurred in database
"aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502",
table "dbo.Host", column 'HostID'. The statement has been terminated.
有什么想法吗?
我们做过的事情:
如果我们从 SaveChangesAsyc()
方法中删除 OnBeforeSaving();
,代码是 成功删除(硬删除) TournamentBatch 作为以及 TournamentBatchItem。
如果我们从 SaveChangesAsyc()
方法中包含 OnBeforeSaving();
并通过删除 Host 和 TournamentBatchItem[= 进行测试65=](不是TournamentBatch),代码是删除(软删除)成功。
好像跟Host和TournamentBatch的关系有关系
环境:
- .Net Core 2.1
- 女士SQL服务器
不要忘记外键是对不同 table 中唯一值的引用。如果存在外键,SQL 将确保引用完整性,因此它不会让您使用孤立的键引用。
当您向外键列插入一个值时,它必须是 null 或对另一个 table 中某行的现有引用,而当您删除时,您必须删除包含外键的行首先,然后是它引用的行。
如果你不这样做,你会得到你所说的错误。
所以先输入"main"table行,然后再输入"dependant"table信息
当您在 EF 中更新有关主键或外键的任何内容时,通常会引发错误。可以 fix this manually.
但是我个人做的事情是删除整个数据库,添加迁移并更新数据库。如果我有很多测试数据,可能会生成一个插入脚本。 (这显然在生产环境中不起作用,但是你不应该像在生产环境中那样更改数据库,而是添加一个带有时间戳的可空列,该时间戳指示删除时间,或者如果它是活动的则为空记录。)
您能否尝试以下操作并更改实现软删除的方式。
在您的 ApplicationDBContext
OnBeforeSaving
方法中更改下面的代码
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
---- 至 -----
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
SoftDelete(entry);
}
软删除方法:
private void SoftDelete(DbEntityEntry entry)
{
Type entryEntityType = entry.Entity.GetType();
string tableName = GetTableName(entryEntityType);
string primaryKeyName = GetPrimaryKeyName(entryEntityType);
string sql =
string.Format(
"UPDATE {0} SET IsDeleted = true WHERE {1} = @id",
tableName, primaryKeyName);
Database.ExecuteSqlCommand(
sql,
new SqlParameter("@id", entry.OriginalValues[primaryKeyName]));
// prevent hard delete
entry.State = EntityState.Detached;
}
此方法将对每个删除的实体执行 sql 查询:
UPDATE TournamentBatch SET IsDeleted = true WHERE TournamentBatchID = 123
为了使其通用并与任何实体(不仅仅是 TournamentBatch)兼容,我们需要知道两个额外的属性,Table 名称和主键名称
为此,SoftDelete 方法中有两个函数:GetTableName 和 GetPrimaryKeyName。我在单独的文件中定义了它们并将 class 标记为部分。因此,请务必使您的上下文 class 部分,以使事情正常进行。这是带有缓存机制的 GetTableName 和 GetPrimaryKeyName:
public partial class ApplicationDBContext
{
private static Dictionary<Type, EntitySetBase> _mappingCache =
new Dictionary<Type, EntitySetBase>();
private string GetTableName(Type type)
{
EntitySetBase es = GetEntitySet(type);
return string.Format("[{0}].[{1}]",
es.MetadataProperties["Schema"].Value,
es.MetadataProperties["Table"].Value);
}
private string GetPrimaryKeyName(Type type)
{
EntitySetBase es = GetEntitySet(type);
return es.ElementType.KeyMembers[0].Name;
}
private EntitySetBase GetEntitySet(Type type)
{
if (!_mappingCache.ContainsKey(type))
{
ObjectContext octx = ((IObjectContextAdapter)this).ObjectContext;
string typeName = ObjectContext.GetObjectType(type).Name;
var es = octx.MetadataWorkspace
.GetItemCollection(DataSpace.SSpace)
.GetItems<EntityContainer>()
.SelectMany(c => c.BaseEntitySets
.Where(e => e.Name == typeName))
.FirstOrDefault();
if (es == null)
throw new ArgumentException("Entity type not found in GetTableName", typeName);
_mappingCache.Add(type, es);
}
return _mappingCache[type];
}
}
原因
我想原因是您要从客户端绑定 TournamentBatch
。
让我们回顾一下 OnPostAsync()
方法:
public async Task<IActionResult> OnPostAsync(Guid? id)
{
try
{
if (await _context.TournamentBatch.AnyAsync(
m => m.TournamentBatchID == id))
{
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
// ....
}
这里TournamentBatch
是PageModel的属性:
[BindProperty]
public Models.TournamentBatch TournamentBatch{ get; set; }
注意你没有根据id从数据库中获取,你直接直接_context.TournamentBatch.Remove(TournamentBatch);
移除[=50] =].
换句话说,TournamentBatch
的其他属性将由ModelBinding设置。比方说,如果您只提交 Id,所有其他 属性 将是默认值。例如,Host
将为 null,而 HostID
将为默认值 00000000-0000-0000-0000-000000000000
。因此,当您保存更改时,EF Core 将更新模型如下:
UPDATE [TournamentBatch]
SET [HostID] = '00000000-0000-0000-0000-000000000000' ,
[IsDeleted] = 1 ,
# ... other fields
WHERE [TournamentBatchID] = 'A6F5002A-60CA-4B45-D343-08D660167B06'
因为没有id等于00000000-0000-0000-0000-000000000000
的主机记录,数据库会报错:
The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". The conflict occurred in database "App-93a194ca-9622-487c-94cf-bcbe648c6556", table "dbo.Host", column 'Id'.
The statement has been terminated.
如何修复
不是从客户端绑定 TournamentBatch
,而是需要通过 TournamentBatch = await _context.TournamentBatch.FindAsync(id);
从服务器检索 TournamentBatch
。因此,您将正确设置所有属性,以便 EF 正确更新字段:
try
{
//var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
//_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
//await _context.SaveChangesAsync();
TournamentBatch = await _context.TournamentBatch.FindAsync(id);
if (TournamentBatch != null)
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
// ...
我们有 3 个模型 类:
- 主持人
- TournamentBatch
- TournamentBatchItem
Host 有很多 TournamentBatch。 TournamentBatch 有很多 TournamentBatchItem。在 TournamentBatch table 中将有 FK Host。
我们确实重写了 ApplicationDbContext 中的 SaveChangesAsync 以允许软删除,如下所示:
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
// Added
var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
added.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
((IBaseEntity)entry.Entity).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Modified
var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
modified.ForEach(entry =>
{
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
// var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted).ToList();
deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
在我们的模型中:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace AthlosifyWebArchery.Models
{
public class TournamentBatch : IBaseEntity
{
[Key]
public Guid TournamentBatchID { get; set; }
public Guid HostID { get; set; }
public string Name { get; set; }
public string BatchFilePath { get; set; }
[Display(Name = "Batch File Size (bytes)")]
[DisplayFormat(DataFormatString = "{0:N1}")]
public long BatchFileSize { get; set; }
[Display(Name = "Uploaded (UTC)")]
[DisplayFormat(DataFormatString = "{0:F}")]
public DateTime DateUploaded { get; set; }
public DateTime DateCreated { get; set; }
public string CreatedBy { get; set; }
public DateTime LastDateModified { get; set; }
public string LastModifiedBy { get; set; }
public DateTime? DateDeleted { get; set; }
public string DeletedBy { get; set; }
public bool IsDeleted { get; set; }
public Host Host { get; set; }
public ICollection<TournamentBatchItem> TournamentBatchItems { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
[ForeignKey("CreatedBy")]
public ApplicationUser ApplicationCreatedUser { get; set; }
[ForeignKey("LastModifiedBy")]
public ApplicationUser ApplicationLastModifiedUser { get; set; }
}
}
在我们的 Razorpage 中,我们有一个页面可以删除包含 TournamentBatchItem 的 TournamentBatch,方法是:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using Microsoft.Extensions.Logging;
namespace AthlosifyWebArchery.Pages.Administrators.TournamentBatches
{
public class DeleteModel : PageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly ILogger _logger;
public DeleteModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
ILogger<DeleteModel> logger)
{
_context = context;
_logger = logger;
}
[BindProperty]
public TournamentBatch TournamentBatch { get; set; }
public IList<TournamentBatchItem> tournamentBatchItems { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(Guid? id, bool? concurrencyError)
{
if (id == null)
{
return NotFound();
}
TournamentBatch = await _context.TournamentBatch
.AsNoTracking() //Addded
.FirstOrDefaultAsync(m => m.TournamentBatchID == id);
if (TournamentBatch == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(Guid? id)
{
try
{
//var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
//_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
//await _context.SaveChangesAsync();
if (await _context.TournamentBatch.AnyAsync(
m => m.TournamentBatchID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
catch(DbUpdateException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
//catch (DbUpdateConcurrencyException)
//{
// return RedirectToPage("./Delete",
// new { concurrencyError = true, id = id });
//}
}
}
}
... 我们有以下有点奇怪的错误。
System.Data.SqlClient.SqlException (0x80131904): The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". The conflict occurred in database "aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502", table "dbo.Host", column 'HostID'. The statement has been terminated.
有什么想法吗?
我们做过的事情:
如果我们从
SaveChangesAsyc()
方法中删除OnBeforeSaving();
,代码是 成功删除(硬删除) TournamentBatch 作为以及 TournamentBatchItem。如果我们从
SaveChangesAsyc()
方法中包含OnBeforeSaving();
并通过删除 Host 和 TournamentBatchItem[= 进行测试65=](不是TournamentBatch),代码是删除(软删除)成功。
好像跟Host和TournamentBatch的关系有关系
环境:
- .Net Core 2.1
- 女士SQL服务器
不要忘记外键是对不同 table 中唯一值的引用。如果存在外键,SQL 将确保引用完整性,因此它不会让您使用孤立的键引用。
当您向外键列插入一个值时,它必须是 null 或对另一个 table 中某行的现有引用,而当您删除时,您必须删除包含外键的行首先,然后是它引用的行。
如果你不这样做,你会得到你所说的错误。
所以先输入"main"table行,然后再输入"dependant"table信息
当您在 EF 中更新有关主键或外键的任何内容时,通常会引发错误。可以 fix this manually.
但是我个人做的事情是删除整个数据库,添加迁移并更新数据库。如果我有很多测试数据,可能会生成一个插入脚本。 (这显然在生产环境中不起作用,但是你不应该像在生产环境中那样更改数据库,而是添加一个带有时间戳的可空列,该时间戳指示删除时间,或者如果它是活动的则为空记录。)
您能否尝试以下操作并更改实现软删除的方式。
在您的 ApplicationDBContext
OnBeforeSaving
方法中更改下面的代码
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
---- 至 -----
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
SoftDelete(entry);
}
软删除方法:
private void SoftDelete(DbEntityEntry entry)
{
Type entryEntityType = entry.Entity.GetType();
string tableName = GetTableName(entryEntityType);
string primaryKeyName = GetPrimaryKeyName(entryEntityType);
string sql =
string.Format(
"UPDATE {0} SET IsDeleted = true WHERE {1} = @id",
tableName, primaryKeyName);
Database.ExecuteSqlCommand(
sql,
new SqlParameter("@id", entry.OriginalValues[primaryKeyName]));
// prevent hard delete
entry.State = EntityState.Detached;
}
此方法将对每个删除的实体执行 sql 查询:
UPDATE TournamentBatch SET IsDeleted = true WHERE TournamentBatchID = 123
为了使其通用并与任何实体(不仅仅是 TournamentBatch)兼容,我们需要知道两个额外的属性,Table 名称和主键名称
为此,SoftDelete 方法中有两个函数:GetTableName 和 GetPrimaryKeyName。我在单独的文件中定义了它们并将 class 标记为部分。因此,请务必使您的上下文 class 部分,以使事情正常进行。这是带有缓存机制的 GetTableName 和 GetPrimaryKeyName:
public partial class ApplicationDBContext
{
private static Dictionary<Type, EntitySetBase> _mappingCache =
new Dictionary<Type, EntitySetBase>();
private string GetTableName(Type type)
{
EntitySetBase es = GetEntitySet(type);
return string.Format("[{0}].[{1}]",
es.MetadataProperties["Schema"].Value,
es.MetadataProperties["Table"].Value);
}
private string GetPrimaryKeyName(Type type)
{
EntitySetBase es = GetEntitySet(type);
return es.ElementType.KeyMembers[0].Name;
}
private EntitySetBase GetEntitySet(Type type)
{
if (!_mappingCache.ContainsKey(type))
{
ObjectContext octx = ((IObjectContextAdapter)this).ObjectContext;
string typeName = ObjectContext.GetObjectType(type).Name;
var es = octx.MetadataWorkspace
.GetItemCollection(DataSpace.SSpace)
.GetItems<EntityContainer>()
.SelectMany(c => c.BaseEntitySets
.Where(e => e.Name == typeName))
.FirstOrDefault();
if (es == null)
throw new ArgumentException("Entity type not found in GetTableName", typeName);
_mappingCache.Add(type, es);
}
return _mappingCache[type];
}
}
原因
我想原因是您要从客户端绑定 TournamentBatch
。
让我们回顾一下 OnPostAsync()
方法:
public async Task<IActionResult> OnPostAsync(Guid? id)
{
try
{
if (await _context.TournamentBatch.AnyAsync(
m => m.TournamentBatchID == id))
{
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
// ....
}
这里TournamentBatch
是PageModel的属性:
[BindProperty]
public Models.TournamentBatch TournamentBatch{ get; set; }
注意你没有根据id从数据库中获取,你直接直接_context.TournamentBatch.Remove(TournamentBatch);
移除[=50] =].
换句话说,TournamentBatch
的其他属性将由ModelBinding设置。比方说,如果您只提交 Id,所有其他 属性 将是默认值。例如,Host
将为 null,而 HostID
将为默认值 00000000-0000-0000-0000-000000000000
。因此,当您保存更改时,EF Core 将更新模型如下:
UPDATE [TournamentBatch]
SET [HostID] = '00000000-0000-0000-0000-000000000000' ,
[IsDeleted] = 1 ,
# ... other fields
WHERE [TournamentBatchID] = 'A6F5002A-60CA-4B45-D343-08D660167B06'
因为没有id等于00000000-0000-0000-0000-000000000000
的主机记录,数据库会报错:
The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". The conflict occurred in database "App-93a194ca-9622-487c-94cf-bcbe648c6556", table "dbo.Host", column 'Id'. The statement has been terminated.
如何修复
不是从客户端绑定 TournamentBatch
,而是需要通过 TournamentBatch = await _context.TournamentBatch.FindAsync(id);
从服务器检索 TournamentBatch
。因此,您将正确设置所有属性,以便 EF 正确更新字段:
try
{
//var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
//_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
//await _context.SaveChangesAsync();
TournamentBatch = await _context.TournamentBatch.FindAsync(id);
if (TournamentBatch != null)
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.TournamentBatch.Remove(TournamentBatch);
_logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
await _context.SaveChangesAsync();
_logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
}
return RedirectToPage("./Index");
}
// ...