Dato un database, il monitoraggio delle persone e il loro coniuge opzionale come chiave esterna autoreferenziale:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int? SpouseId { get; set; }
}
Entity Framwork Core DbContext si presenta come questo, notare DeleteBehavior.SetNull
:
public class PersonsContext : DbContext
{
public PersonsContext(DbContextOptions<PersonsContext> options) : base(options) {}
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<Person>()
.HasOne(typeof(Person))
.WithOne()
.HasForeignKey(typeof(Person), nameof(Person.SpouseId))
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
}
}
Questo non genera nemmeno un modello. L'errore dice:
L'introduzione del vincolo FOREIGN KEY "FK_Persons_Persons_SpouseId" nella tabella "Persone" può causare cicli o più percorsi a cascata. Specificare ON DELETE NO ACTION o ON UPDATE NO ACTION o modificare altri vincoli FOREIGN KEY.
Ok, secondo tentativo. Ci stiamo occupando di rompere il riferimento per conto nostro. L'FK sarà modellato con DeleteBehavior.Restrict
:
public class PersonsContext : DbContext
{
public PersonsContext(DbContextOptions<PersonsContext> options) : base(options) {}
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder
.Entity<Person>()
.HasOne(typeof(Person))
.WithOne()
.HasForeignKey(typeof(Person), nameof(Person.SpouseId))
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
}
}
Un semplice test tenta di annullare il riferimento su entrambi i lati e quindi elimina una persona sposata, lasciando l'altra persona senza un SpouseId
:
[Fact]
public void Manually_Remove_Reference()
{
using (var personsContext = new PersonsContext(DbContextOptions))
{
var him = new Person {Id = 1, Name = "Him", SpouseId = 2};
var her = new Person {Id = 2, Name = "Her", SpouseId = 1};
personsContext.Persons.Add(him);
personsContext.Persons.Add(her);
personsContext.SaveChanges();
}
using (var personsContext = new PersonsContext(DbContextOptions))
{
var him = personsContext.Persons.Find(1);
var her = personsContext.Persons.Find(2);
him.SpouseId = null;
her.SpouseId = null;
personsContext.Persons.Remove(him);
personsContext.SaveChanges();
}
using (var personsContext = new PersonsContext(DbContextOptions))
{
Assert.Null(personsContext.Find<Person>(1));
}
}
Con il risultato di:
System.InvalidOperationException: impossibile salvare le modifiche perché è stata rilevata una dipendenza circolare nei dati da salvare: "ForeignKey: Person {'SpouseId'} -> Person {'Id'} Unique, ForeignKey: Person {'SpouseId'} -> Person {'Id'} Unique '.
Stack Trace:
at Microsoft.EntityFrameworkCore.Internal.Multigraph`2.BatchingTopologicalSort(Func`2 formatCycle)
at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.TopologicalSort(IEnumerable`1 commands)
at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.<BatchCommands>d__8.MoveNext()
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(Tuple`2 parameters)
at Microsoft.EntityFrameworkCore.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.ExecutionStrategyExtensions.Execute[TState,TResult](IExecutionStrategy strategy, TState state, Func`2 operation)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IReadOnlyList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
at Persistence.Tests.UnitTest1.Manually_Remove_Reference() in C:\Users\MarcWittke\source\scratchpad\Persistence\Persistence.Tests\UnitTest1.cs:line 58
L'aggiunta di ulteriori SaveChanges()
dopo aver annullato i riferimenti non fa differenza. Ciò che effettivamente funziona è: interrompere il riferimento in un'istanza di DbContext separata, salvarlo, aprirne uno nuovo ed eliminare il record. Ma questo non sarà più atomico.
Codice completo: https://github.com/marcwittke/DeletingOptionallySelfReferencedRecord
Non importa, il problema è stato il mio setup di prova, inserendo le due persone. (nota a me stesso: controlla i numeri di linea nelle tracce dello stack)
questo codice funziona come previsto:
[Fact]
public void Manually_Remove_Reference()
{
using (var personsContext = new PersonsContext(DbContextOptions))
{
var him = new Person { Name = "Him"};
var her = new Person { Name = "Her"};
personsContext.Persons.Add(him);
personsContext.Persons.Add(her);
personsContext.SaveChanges();
// this must occur after inserting the two persons!!
him.SpouseId = her.Id;
her.SpouseId = him.Id;
personsContext.SaveChanges();
}
using (var personsContext = new PersonsContext(DbContextOptions))
{
var her = personsContext.Persons.Find(2);
her.SpouseId = null;
var him = personsContext.Persons.Find(1);
personsContext.Persons.Remove(him);
personsContext.SaveChanges();
}
using (var personsContext = new PersonsContext(DbContextOptions))
{
Assert.Null(personsContext.Find<Person>(1));
}
}
È possibile assegnare un solo EnityState a un'entità tracciata in qualsiasi momento. Quindi, quando si imposta him.SpouseId == null
quell'entità ha uno stato di EntityState.Modified
, ma non appena si chiama personsContext.Remove(him)
lo stato è ora EntityState.Deleted
. EF non terrà traccia delle variazioni di ordine di stato in questo modo, solo lo stato corrente dell'entità.
Per correggere il problema, devi chiamare .SaveChanges()
non appena modifichi i campi FK su null, quindi rimuovi le entità e salva di nuovo le modifiche.
using (var personsContext = new PersonsContext(DbContextOptions))
{
var him = personsContext.Persons.Find(1);
var her = personsContext.Persons.Find(2);
him.SpouseId = null;
her.SpouseId = null;
personsContext.SaveChanges(); // Add this line
personsContext.Persons.Remove(him);
personsContext.SaveChanges();
}