简体   繁体   English

UPDATE语句与EF Core中的FOREIGN KEY约束冲突

[英]The UPDATE statement conflicted with the FOREIGN KEY constraint in EF Core

We have 3 model classes: 我们有3个模型类:

  • Host 主办
  • TournamentBatch TournamentBatch
  • TournamentBatchItem TournamentBatchItem

Host has many TournamentBatch. 主持人有很多TournamentBatch。 TournamentBatch has many TournamentBatchItem. TournamentBatch有很多TournamentBatchItem。 In the TournamentBatch table will have FK Host. 在TournamentBatch表中将有FK Host。

We did override for SaveChangesAsync in ApplicationDbContext to allow soft-delete as following: 我们在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
        }
    }

In our model: 在我们的模型中:

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


}

} }

In our Razorpage, we have a page to delete TournamentBatch including TournamentBatchItem by doing this: 在我们的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 });
        //}
    }
}

} }

... and we have the following error which is a bit odd. ...我们有以下错误,这有点奇怪。

System.Data.SqlClient.SqlException (0x80131904): The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". System.Data.SqlClient.SqlException(0x80131904):UPDATE语句与FOREIGN KEY约束“FK_TournamentBatch_Host_HostID”冲突。 The conflict occurred in database "aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502", table "dbo.Host", column 'HostID'. 冲突发生在数据库“aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502”,表“dbo.Host”,列'HostID'。 The statement has been terminated. 该语句已终止。

Any ideas? 有任何想法吗?

Things we did: 我们做的事情:

  • If we removed OnBeforeSaving(); 如果我们删除了OnBeforeSaving(); from the SaveChangesAsyc() method, the code is deleting (hard-delete) successfully the TournamentBatch as well as TournamentBatchItem. SaveChangesAsyc()方法,代码正在成功删除(硬删除) TournamentBatch以及TournamentBatchItem。

  • If we included OnBeforeSaving(); 如果我们包含OnBeforeSaving(); from the SaveChangesAsyc() method AND tested with deleting Host and TournamentBatchItem (Not TournamentBatch ), the code is deleting (soft-delete) successfully . SaveChangesAsyc()方法和使用删除HostTournamentBatchItem (非TournamentBatch )测试,代码正在成功删除(软删除)

It seems it has something to do with the relationship between Host and TournamentBatch 它似乎与Host和TournamentBatch之间的关系有关

Environment: 环境:

  • .Net Core 2.1 .Net Core 2.1
  • Ms SQL Server SQL Server女士

Can you try the following and change how you implemeted the soft-delete. 您可以尝试以下操作并更改实现软删除的方式。

Change the code below in your ApplicationDBContext OnBeforeSaving method 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 method: 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;
}

This method will execute sql query over each removed entity: 此方法将对每个已删除的实体执行sql查询:

UPDATE TournamentBatch SET IsDeleted = true WHERE TournamentBatchID = 123

To make it versatile and compatible with any entity (not just TournamentBatch) we need to know two additional properties, Table name and Primary Key name 为了使它具有多功能性并与任何实体(不仅仅是TournamentBatch)兼容,我们需要知道另外两个属性,即表名和主键名

There are two functions inside of SoftDelete method for this purpose: GetTableName and GetPrimaryKeyName. 为此,SoftDelete方法中有两个函数:GetTableName和GetPrimaryKeyName。 I have defined them in separate file and marked class as partial. 我已将它们定义在单独的文件中,并将其标记为部分类。 So be sure to make your context class partial in order for things to work. 因此,请务必使您的上下文类成为局部,以使事情顺利进行。 Here is GetTableName and GetPrimaryKeyName with caching mechanism: 这是带有缓存机制的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];
    }
}

Reason 原因

I guess the reason is you're having your TournamentBatch bind from client side . 我想原因是你从客户端获得了TournamentBatch绑定。

Let's review the OnPostAsync() method: 我们来看看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");
    }
    // ....
}

Here the TournamentBatch is a property of PageModel : TournamentBatch是PageModel的一个属性

    [BindProperty]
    public Models.TournamentBatch TournamentBatch{ get; set; }

Note you didn't retrieve it from the database according to the id , and you just remove it by _context.TournamentBatch.Remove(TournamentBatch); 请注意, 您没有根据id从数据库中检索它 ,只需通过_context.TournamentBatch.Remove(TournamentBatch);删除它_context.TournamentBatch.Remove(TournamentBatch); directly . 直接

In other words, the other properties of TournamentBatch will be set by ModelBinding. 换句话说, TournamentBatch的其他属性将由ModelBinding设置。 Let's say if you submit only the Id, all the other property will be the default value. 假设您只提交了Id,则所有其他属性都将是默认值。 For example, Host will be null and the HostID will be the default 00000000-0000-0000-0000-000000000000 . 例如, Host将为null, HostID将是默认值00000000-0000-0000-0000-000000000000 So when you save changes, the EF Core will update the model as below : 因此,当您保存更改时,EF Core将更新模型,如下所示:

UPDATE [TournamentBatch]
SET [HostID] = '00000000-0000-0000-0000-000000000000' , 
    [IsDeleted] = 1 ,
    # ... other fields
WHERE [TournamentBatchID] = 'A6F5002A-60CA-4B45-D343-08D660167B06'

Because there's no Host record whose id equals 00000000-0000-0000-0000-000000000000 , the database will complains : 因为没有主机记录的id等于00000000-0000-0000-0000-000000000000 ,数据库会抱怨:

The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". UPDATE语句与FOREIGN KEY约束“FK_TournamentBatch_Host_HostID”冲突。 The conflict occurred in database "App-93a194ca-9622-487c-94cf-bcbe648c6556", table "dbo.Host", column 'Id'. 冲突发生在数据库“App-93a194ca-9622-487c-94cf-bcbe648c6556”,表“dbo.Host”,列'Id'中。 The statement has been terminated. 该语句已终止。

How to fix 怎么修

Instead of binding the TournamentBatch from client side, you need retrieve the TournamentBatch from server by TournamentBatch = await _context.TournamentBatch.FindAsync(id); 您不需要从客户端绑定TournamentBatch ,而是需要通过TournamentBatch = await _context.TournamentBatch.FindAsync(id);从服务器检索TournamentBatch TournamentBatch = await _context.TournamentBatch.FindAsync(id); . Thus you will have all the properties set correctly so that EF will update the field correctly : 因此,您将正确设置所有属性,以便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");
    }
    // ...

Don't forget that a foreign key is a reference to a unique value in a different table. 不要忘记外键是对不同表中唯一值的引用。 SQL will ensure referential integrity if there is a foreign key present, so it won't let you use orphaned key references. 如果存在外键,SQL将确保引用完整性,因此它不允许您使用孤立的键引用。

When you insert a value to a foreign key column it must be a null or a existing reference to a row in the other table, and when you delete, you must delete the row containing the foreign key first, then the row it references. 向外键列插入值时,它必须是null或对另一个表中的行的现有引用,并且在删除时,必须先删除包含外键的行,然后删除它引用的行。

If you don't, you will get an error as you stated. 如果不这样做,您将收到错误,如您所述。

So enter the row into the "main" table first, then enter the "dependant" table information after. 所以先将行输入“main”表,然后输入“dependent”表信息。

When you update anything regarding primary or foreign keys in EF, more often than not, an error is thrown. 当您更新有关EF中的主键或外键的任何内容时,通常会引发错误。 It is possible to fix this manually . 可以手动修复此问题

However the thing I personally do is to drop the entire database, add a migration and update the DB. 但是我个人做的事情是删除整个数据库,添加迁移并更新数据库。 Possibly generating an insert script if i have a lot of test data. 如果我有很多测试数据,可能会生成一个插入脚本。 (this obviously doesn't work in a production environment, but then again you should not change the db like that in a production environment anyway and instead add a nullable column with a timestamp which indicates the time of deletion or be null if its an active record.) (这显然在生产环境中不起作用,但是你不应该再次改变生产环境中的数据库,而是添加一个可以为空的列,其时间戳表示删除的时间,如果它是活动的,则为null记录。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 EF Core 2:MERGE 语句与 FOREIGN KEY 约束冲突 - EF Core 2: The MERGE statement conflicted with the FOREIGN KEY constraint EF Core:INSERT语句与FOREIGN KEY约束冲突 - EF Core: The INSERT statement conflicted with the FOREIGN KEY constraint INSERT语句与EF Core中的FOREIGN KEY约束冲突 - The INSERT statement conflicted with the FOREIGN KEY constraint in EF Core EF Core - MERGE 语句与 FOREIGN KEY 约束冲突 - EF Core - The MERGE statement conflicted with the FOREIGN KEY constraint UPDATE语句与FOREIGN KEY约束冲突 - The UPDATE statement conflicted with the FOREIGN KEY constraint UPDATE语句与FOREIGN KEY约束错误冲突? - The UPDATE statement conflicted with the FOREIGN KEY constraint error? UPDATE语句与FOREIGN KEY约束冲突 - The UPDATE statement conflicted with the FOREIGN KEY constraint EF核心中的自引用错误:插入语句与外键同表约束冲突 - Self-referencing in EF core Error: the insert statement conflicted with the foreign key same table constraint 更新实体/模型时发生EF错误:UPDATE语句与FOREIGN KEY约束冲突 - EF error on updating entity/model :The UPDATE statement conflicted with the FOREIGN KEY constraint EF Code First INSERT 语句与 FOREIGN KEY 约束冲突 - EF Code First The INSERT statement conflicted with the FOREIGN KEY constraint
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM