【问题标题】:The UPDATE statement conflicted with the FOREIGN KEY constraint in EF CoreUPDATE 语句与 EF Core 中的 FOREIGN KEY 约束冲突
【发布时间】:2019-05-09 17:17:59
【问题描述】:

我们有 3 个模型类:

  • 主持人
  • TournamentBatch
  • TournamentBatchItem

Host 有很多 TournamentBatch。 TournamentBatch 有很多 TournamentBatchItem。在 TournamentBatch 表中会有 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 中,我们有一个页面来删除 TournamentBatch 包括 TournamentBatchItem 这样做:

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):UPDATE 语句 与 FOREIGN KEY 约束冲突 “FK_TournamentBatch_Host_HostID”。数据库发生冲突 "aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502", 表“dbo.Host”,列“HostID”。声明已终止。

有什么想法吗?

我们做过的事情:

  • 如果我们从 SaveChangesAsyc() 方法中删除 OnBeforeSaving();,代码将成功删除(硬删除) TournamentBatch 以及 TournamentBatchItem。

  • 如果我们从 SaveChangesAsyc() 方法中包含 OnBeforeSaving(); 并通过删除 HostTournamentBatchItem 进行测试(不是 TournamentBatch) ,代码删除(软删除)成功

好像和Host和TournamentBatch的关系有关

环境:

  • .Net Core 2.1
  • SQL Server 女士

【问题讨论】:

  • 我试过你的代码,但我没有抛出异常。但是,错误信息表明您的代码正在后台更新外键。我建议您可以检查客户端发送的有效负载并检查 sql 日志。
  • @itminus ...不太明白,因为我们根本没有更新外键。正如您在我们的代码中看到的那样。
  • 发帖到action方法时,TournamentBatch是什么?
  • 它是 ApplicationDbContext 类的属性。 public DbSet&lt;AthlosifyWebArchery.Models.TournamentBatch&gt; TournamentBatch { get; set; }
  • 我的意思是当您调用`await _context.SaveChangesAsync();` 时,TournamentBatch 或其他实体必须有一些更改,对吧?那么这些实体是什么样的呢?而当你保存更改并抛出异常时,幕后的sql语句是什么?

标签: c# asp.net-core entity-framework-core asp.net-core-2.0 asp.net-core-2.1


【解决方案1】:

原因

我猜原因是您的 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);删除

也就是说,TournamentBatch 的其他属性将由 ModelBinding 设置。假设您仅提交 Id,则所有其他属性将是默认值。例如,Host 将为空,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的Host记录,数据库会报错:

UPDATE 语句与 FOREIGN KEY 约束“FK_TournamentBatch_Host_HostID”冲突。冲突发生在数据库“App-93a194ca-9622-487c-94cf-bcbe648c6556”、表“dbo.Host”、“Id”列中。 声明已终止。

如何解决

您需要通过TournamentBatch = await _context.TournamentBatch.FindAsync(id); 从服务器检索TournamentBatch,而不是从客户端绑定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");
    }
    // ...

【讨论】:

  • 感谢您的解决方案。效果很好。没有意识到 [BindProperty] 导致了这种情况。它没有解释为什么 HARD-DELETE 工作正常。总之谢谢
  • 另外......通过这种方法,子对象不会只更新父对象(UPDATE XX SET IsDelete=1)。这是正确的行为吗?所以我只是为子对象应用类似的。因为如果您进行硬删除,孩子将被自动删除。
  • @dcpartners 它确实解释了HARD-DELETESOFT-DELETE 使用 update,这意味着 SQL 将是 update ... set hostid = .. where ...[TournamentBatchID] = ...,但 HARD-DELETE 使用 delete ...where [TournamentBatchID] = ... '
  • @dcpartners 如果你确实想要级联软删除的功能,你可以参考这个 [walkaround here](github.com/aspnet/EntityFrameworkCore/issues/…)。稍后我会测试这个解决方法。
  • @dcpartners 至于第二个问题,EF Core 将自动删除子项,因为它知道应该删除此依赖记录以及删除主体记录。但是,如果您使用软删除,EF 根本不知道您要删除该主体记录
【解决方案2】:

您能否尝试以下方法并更改您实现软删除的方式。

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;
    }
}

---- TO -----

foreach (var entry in ChangeTracker.Entries()
                                    .Where(e => e.State == EntityState.Deleted &&
                                    e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
    SoftDelete(entry);
}

SoftDelete 方法:

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)兼容,我们需要知道两个附加属性,表名和主键名

为此,SoftDelete 方法内部有两个函数:GetTableName 和 GetPrimaryKeyName。我已在单独的文件中定义它们并将类标记为部分。因此,请务必使您的上下文类部分化,以使事情正常工作。这是带有缓存机制的 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];
    }
}

【讨论】:

    【解决方案3】:

    不要忘记外键是对不同表中唯一值的引用。如果存在外键,SQL 将确保引用完整性,因此它不会让您使用孤立键引用。

    向外键列插入值时,该值必须为空值或对另一个表中行的现有引用,而在删除时,必须先删除包含外键的行,然后再删除包含外键的行参考文献。

    如果你不这样做,你会得到你所说的错误。

    所以先将行输入到“主”表中,然后再输入“从属”表信息。

    【讨论】:

    • 我们知道,但一切都在幕后处理。我们没有对外键做任何更新......只做改变状态的软删除
    • 先删除子表再删除父表时
    • 我们没有删除记录...我们执行软删除 (UPDATE xx SET IsDeleted = 1)
    • 我添加了另一个答案,因为这个答案也可以帮助其他有同样错误的人,而我的第二个答案与这个答案无关
    【解决方案4】:

    当您在 EF 中更新有关主键或外键的任何内容时,通常会引发错误。可以fix this manually

    但是我个人所做的事情是删除整个数据库,添加迁移并更新数据库。如果我有很多测试数据,可能会生成一个插入脚本。 (这显然在生产环境中不起作用,但是你不应该像在生产环境中那样更改数据库,而是添加一个带有时间戳的可空列,该时间戳指示删除时间,如果它是活动的,则为空记录。)

    【讨论】:

    • 解决方案到底是什么?我有点迷路了..对不起:)
    猜你喜欢
    • 2018-10-12
    • 2023-03-06
    • 2017-02-28
    • 2019-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多