Оператор UPDATE конфликтует с ограничением FOREIGN KEY в EF Core - PullRequest
0 голосов
/ 08 декабря 2018

У нас есть 3 класса моделей:

  • Хост
  • TournamentBatch
  • TournamentBatchItem

Хост имеет много TournamentBatch.TournamentBatch имеет много элементов TournamentBatchItem.В таблице TournamentBatch появится FK Host.

Мы сделали переопределение для SaveChangesAsync в ApplicationDbContext, чтобы разрешить мягкое удаление следующим образом:

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


}

}

На нашей странице Razor у нас есть страница для удаления 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».Утверждение было прекращено.

Есть идеи?

То, что мы сделали:

  • Если мы удалили OnBeforeSaving(); из SaveChangesAsyc() метод, код удаляет (жестко удаляет) успешно TournamentBatch, а также TournamentBatchItem.

  • Если мы включили OnBeforeSaving(); из метода SaveChangesAsyc() И проверили удаление Host и TournamentBatchItem (Not TournamentBatch ), код удаление (мягкое удаление) успешно .

Кажется, это как-то связано с отношениями между Host и TournamentBatch

Среда:

  • .Net Core 2.1
  • MS SQL Server

Ответы [ 4 ]

0 голосов
/ 12 декабря 2018

Причина

Я думаю, причина в том, что у вас есть 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; }

Примечание вы не извлекли его из базы данных по идентификатору , и вы просто удалили его _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'

Поскольку нет записи хоста, чей идентификатор равен 00000000-0000-0000-0000-000000000000, база данных будет жаловаться:

Оператор UPDATE конфликтовал с ограничением FOREIGN KEY "FK_TournamentBatch_Host_HostID".Конфликт произошел в базе данных «App-93a194ca-9622-487c-94cf-bcbe648c6556», таблица «dbo.Host», столбец «Id».Оператор был прерван.

Как исправить

Вместо того, чтобы связывать TournamentBatch со стороны клиента, вам нужно получить TournamentBatch с серверана TournamentBatch = await _context.TournamentBatch.FindAsync(id);.Таким образом, все свойства будут установлены правильно, поэтому 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");
    }
    // ...
0 голосов
/ 11 декабря 2018

Когда вы обновляете что-либо относительно первичных или внешних ключей в EF, чаще всего выдается ошибка.Можно исправить это вручную .

Однако лично я делаю, чтобы отбросить всю базу данных, добавить миграцию и обновить базу данных.Возможно создание сценария вставки, если у меня много тестовых данных.(это, очевидно, не работает в производственной среде, но, опять же, вы все равно не должны изменять БД, как в производственной среде, а вместо этого добавлять обнуляемый столбец с меткой времени, которая указывает время удаления, или быть нулевым, если он активензапись.)

0 голосов
/ 12 декабря 2018

Можете ли вы попробовать следующее и изменить, как вы реализовали мягкое удаление.

Измените приведенный ниже код в вашем 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];
    }
}
0 голосов
/ 11 декабря 2018

Не забывайте, что внешний ключ - это ссылка на уникальное значение в другой таблице.SQL обеспечит ссылочную целостность, если присутствует внешний ключ, поэтому он не позволит использовать ссылки на потерянные ключи.

Когда вы вставляете значение в столбец внешнего ключа, оно должно быть нулевой или существующей ссылкой на строку в другой таблице, и при удалении вы должны сначала удалить строку, содержащую внешний ключ, а затемстрока, на которую он ссылается.

Если вы этого не сделаете, вы получите сообщение об ошибке, как вы заявили.

Поэтому сначала введите строку в "основную" таблицу, а затем введите "зависимую"таблица информации после.

...