wake-up-neo.com

Die UPDATE-Anweisung widersprach der FOREIGN KEY-Einschränkung in EF Core

Wir haben 3 Modellklassen:

  • Wirt
  • TournamentBatch
  • TournamentBatchItem

Gastgeber hat viele TournamentBatch. TournamentBatch hat viele TournamentBatchItem. In der TournamentBatch-Tabelle hat FK Host.

Wir haben SaveChangesAsync in ApplicationDbContext überschrieben, um das vorläufige Löschen wie folgt zu ermöglichen:

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 unserem Modell:

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 unserer Razorpage haben wir eine Seite zum Löschen von TournamentBatch einschließlich 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 });
        //}
    }
}

}

... und wir haben den folgenden Fehler, der etwas seltsam ist.

System.Data.SqlClient.SqlException (0x80131904): Die UPDATE-Anweisung ist mit der FOREIGN KEY-Einschränkung "FK_TournamentBatch_Host_HostID" in Konflikt geraten. Der Konflikt ist in der Datenbank "aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502", Tabelle "dbo.Host", Spalte "HostID" aufgetreten. Die Anweisung wurde beendet.

Irgendwelche Ideen?

Dinge, die wir getan haben:

  • Wenn wir OnBeforeSaving(); aus der SaveChangesAsyc()-Methode entfernt haben, lautet der Code Löschen (hartes Löschen) erfolgreich TournamentBatch sowie TournamentBatchItem.

  • Wenn wir OnBeforeSaving(); aus der SaveChangesAsyc()method einbezogen UND beim Löschen von Host und TournamentBatchItem (Not TournamentBatch ) getestet haben, lautet der Code erfolgreiches Löschen (soft-delete) .

Es scheint etwas mit der Beziehung zwischen Host und TournamentBatch zu tun zu haben

Umgebung:

  • .Net Core 2.1
  • Frau SQL Server
4
dcpartners

Grund

Ich denke, der Grund dafür ist, dass Sie Ihre TournamentBatch vom Client aus binden lassen.

Sehen wir uns die Methode OnPostAsync() an:

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");
    }
    // ....
}

Hier ist die TournamentBatch eine Eigenschaft von PageModel :

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

Hinweis Sie haben es nicht gemäß der ID aus der Datenbank abgerufen und Sie haben es einfach durch _context.TournamentBatch.Remove(TournamentBatch); direkt entfernt .

Mit anderen Worten, die anderen Eigenschaften von TournamentBatch werden von ModelBinding festgelegt. Angenommen, wenn Sie nur die ID übergeben, wird für alle anderen Eigenschaften der Standardwert verwendet. Beispielsweise ist Host null und die HostID ist der Standard-00000000-0000-0000-0000-000000000000. Wenn Sie Änderungen speichern, aktualisiert der EF Core das Modell wie folgt:

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

Da es keinen Host-Datensatz gibt, dessen ID 00000000-0000-0000-0000-000000000000 ist, beschwert sich die Datenbank über Folgendes:

Die UPDATE-Anweisung stand in Konflikt mit der FOREIGN KEY-Einschränkung "FK_TournamentBatch_Host_HostID". Der Konflikt ist in der Datenbank "App-93a194ca-9622-487c-94cf-bcbe648c6556", Tabelle "dbo.Host", Spalte "Id" aufgetreten. Die Anweisung wurde beendet.

Wie repariert man

Anstatt die TournamentBatch vom Client aus zu binden, müssen Sie die TournamentBatch über TournamentBatch = await _context.TournamentBatch.FindAsync(id); vom Server abrufen. So haben Sie alle Eigenschaften richtig eingestellt, so dass EF das Feld richtig aktualisiert:

    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");
    }
    // ...
1
itminus

Können Sie Folgendes versuchen und ändern, wie Sie das Soft-Delete implementiert haben?.

Ändern Sie den folgenden Code in Ihrer ApplicationDBContextOnBeforeSaving-Methode

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-Methode:

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

Diese Methode führt eine SQL-Abfrage für jede entfernte Entität aus:

UPDATE TournamentBatch SET IsDeleted = true WHERE TournamentBatchID = 123

Damit es vielseitig und mit jeder Entität (nicht nur TournamentBatch) kompatibel ist, müssen zwei zusätzliche Eigenschaften bekannt sein: Tabellenname und Primärschlüsselname

Zu diesem Zweck enthält die SoftDelete-Methode zwei Funktionen: GetTableName und GetPrimaryKeyName. Ich habe sie in einer separaten Datei definiert und die Klasse als teilweise markiert. Stellen Sie also sicher, dass Ihre Kontextklasse partiell ist, damit die Dinge funktionieren. Hier sind GetTableName und GetPrimaryKeyName mit Caching-Mechanismus:

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];
    }
}
2
Tarik Tutuncu

Wenn Sie etwas in Bezug auf Primär- oder Fremdschlüssel in EF aktualisieren, wird häufig ein Fehler ausgegeben. Es ist möglich, dies manuell zu beheben .

Ich persönlich lösche jedoch die gesamte Datenbank, füge eine Migration hinzu und aktualisiere die Datenbank. Möglicherweise wird ein Einfügeskript generiert, wenn ich viele Testdaten habe. (Dies funktioniert natürlich nicht in einer Produktionsumgebung, aber andererseits sollten Sie die Datenbank in einer Produktionsumgebung sowieso nicht so ändern und stattdessen eine nullfähige Spalte mit einem Zeitstempel hinzufügen, der den Zeitpunkt des Löschens angibt, oder null sein, wenn sie aktiv ist Aufzeichnung.)

0
Daan

Vergessen Sie nicht, dass ein Fremdschlüssel auf einen eindeutigen Wert in einer anderen Tabelle verweist. SQL stellt die referenzielle Integrität sicher, wenn ein Fremdschlüssel vorhanden ist, sodass Sie keine verwaisten Schlüsselreferenzen verwenden können.

Wenn Sie einen Wert in eine Fremdschlüsselspalte einfügen, muss es sich um eine Null oder eine vorhandene Referenz auf eine Zeile in der anderen Tabelle handeln. Wenn Sie einen Wert löschen, müssen Sie zuerst die Zeile löschen, die den Fremdschlüssel enthält, und dann die Zeile, auf die sie verweist.

Wenn Sie dies nicht tun, erhalten Sie wie angegeben eine Fehlermeldung.

Geben Sie die Zeile also zuerst in die "Haupt" -Tabelle ein und geben Sie anschließend die "abhängigen" Tabelleninformationen ein.

0
Tarik Tutuncu