Entity Framework Core на SQL Server: не удаляется необязательно ссылка на себя - PullRequest
0 голосов
/ 09 мая 2018

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

public class Person
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int? SpouseId { get; set; }
}

Ядро Framwork Entity DbContext выглядит следующим образом, обратите внимание на 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);
    }
}

Это даже не генерирует модель. Ошибка говорит:

Введение ограничения FOREIGN KEY 'FK_Persons_Persons_SpouseId' в Таблица «Персоны» может вызывать циклы или несколько каскадных путей. Уточнить НА УДАЛИТЬ НЕТ ДЕЙСТВИЙ или НА ОБНОВИТЬ НЕТ ДЕЙСТВИЙ, или изменить другие ИНОСТРАННЫЕ КЛЮЧЕВЫЕ ограничения.


Хорошо, вторая попытка. Мы позаботимся о том, чтобы сломать ссылку самостоятельно. FK будет моделироваться с помощью 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);
    }
}

Простой тест пытается обнулить ссылку с обеих сторон, а затем удаляет женатого человека, оставляя другого человека без 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));
    }
}

В результате:

System.InvalidOperationException: невозможно сохранить изменения, поскольку в данных, которые необходимо сохранить, была обнаружена циклическая зависимость: 'ForeignKey: Person {' SpouseId '} -> Person {' Id '} Уникальный, ForeignKey: Person {' SpouseId ' } -> Person {'Id'} Уникальный '.

Трассировка стека:

   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

Добавление больше SaveChanges() после обнуления ссылок не имеет значения. Что действительно работает, так это: разорвать ссылку в отдельном экземпляре DbContext, сохранить его, открыть новый и удалить запись. Но это больше не будет атомным.

Полный код: https://github.com/marcwittke/DeletingOptionallySelfReferencedRecord

Ответы [ 2 ]

0 голосов
/ 09 мая 2018

Неважно, проблема была в моей настройке теста, вставив двух человек. (обратите внимание: проверьте номера строк в следах стека)

этот код работает как положено:

[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));
    }
}
0 голосов
/ 09 мая 2018

Только один EnityState может быть назначен отслеживаемому объекту в любой момент времени. Поэтому, когда вы устанавливаете him.SpouseId == null, эта сущность имеет состояние EntityState.Modified, но как только вы вызываете personsContext.Remove(him), состояние становится EntityState.Deleted. Таким образом, EF не будет отслеживать порядок изменений состояния, только текущее состояние объекта.

Чтобы исправить вашу проблему, вы должны позвонить .SaveChanges(), как только вы измените поля FK на null, затем удалите объекты и сохраните изменения снова.

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();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...