EF Core удаляет взаимно-однозначное отношение в одной таблице - PullRequest
0 голосов
/ 02 сентября 2018

Модель имеет необязательное отношение к себе

public class Item
{
    public Guid Id { get; set; }
    public string Description { get; set; }
    public Guid StockId { get; set; }

    // optionally reference to another item from different stock
    public Guid? OptionalItemId { get; set; }

    public virtual Item OptionalItem { get; set; }      
}

В модели DbContext, настроенной как показано ниже:

protected override void OnModelCreating(ModelBuilder builder)
{
     builder.Entity<Item>().HasOne(item => item.OptionalItem)
                           .WithOne()
                           .HasForeignKey<Item>(item => item.OptionalItemId)
                           .HasPrincipalKey<Item>(item => item.Id)
                           .IsRequired(false)
}

Я хочу заменить существующие элементы новыми элементами, удалив существующие до обновления Stock новыми элементами.

// Given Stock contains only new items
public void Update(Stock stock)
{
    using (var context = CreateContext())
    {
        // Remove old items
        var oldItems = context.Items
                              .Where(item => item.StockId == stock.Id)
                              .Select(item => new Item { Id = item.Id })
                              .ToList();
        context.Items.RemoveRange(oldItems);

        // Remove optional items from another stock
        var oldOptionalItems = context.Items
                                      .Where(item => item.StockId == stock.RelatedStock.Id)
                                      .Select(item => new Item { Id = item.Id })
                                      .ToList();
        context.Items.RemoveRange(oldOptionalItems);   

        context.Stocks.Update(stock);
        context.SaveChanges();         
    }
}

Проблема в том, что при выполнении метода Update строка context.SaveChanges() выдает исключение:

SqlException: инструкция DELETE конфликтует с той же таблицей ССЫЛОЧНОЕ ограничение "FK_Item_Item_OptionalItemId". произошел конфликт в базе данных "local-database", таблица «dbo.Item», столбец «OptionalItemId».

Я нашел другой вопрос с похожей проблемой: Оператор DELETE конфликтовал с ограничением SAME TABLE REFERENCE с Entity Framework .
Но похоже, что все ответы связаны с Entity Framework (не EF Core).

Я пытался изменить поведение удаления на
- .OnDelete(DeleteBehavior.Cascade)
и
- .OnDelete(DeleteBehavior.SetNull)
но оба поведения приведут к исключению ниже при применении миграции к базе данных.

Введение ограничения FOREIGN KEY 'FK_Item_Item_OptionalItemId' в таблица «Item» может вызывать циклы или несколько каскадных путей.
Укажите ON DELETE NO ACTION или ON UPDATE NO ACTION или измените другие ограничения FOREIGN KEY.

Ответы [ 2 ]

0 голосов
/ 03 сентября 2018

Только в качестве дополнения к ответу @ Ивана.

Item имеют внешний ключ OptionalItem, что означает, что Item зависит от OptionalItem.

`Item`(dependent) -> `OptionalItem`(principal)

EF Core поддерживает «каскадное удаление» от основного к зависимому. Как отметил Иван Стоев, исключением во время миграции является ограничение Sql Server. Но EF Core все равно его поддерживает, вы можете попробовать
- Добавить .OnDelete(DeleteBehavior.Cascade)
- запустить dotnet ef migrations add <migration-name>
- обновить сгенерированный скрипт миграции, удалив действие CASCADE
- обновить базу данных только что созданной миграцией

Вы не получите исключение при применении миграций к базе данных.
Примечание:
1. (опять же) EF Core поддерживает каскадное удаление от основного к зависимому
относящиеся Item будут удалены при удалении записи OptionalItem
2. EF Core автоматически удалит только те связанные записи, которые уже отслежены DbContext (загружены в память)

Так что в вашем случае вы можете попытаться удалить главные элементы (OptionalItem) перед зависимыми Item, но в отдельных командах.
Выполнить все транзакции, поэтому при возникновении ошибки операция будет отменена.

public void Update(Stock stock)
{
    using (var context = CreateContext())
    using (var transaction = context.Database.BeginTransaction())
    {
        // Remove optional items from another stock
        // This is principal record in the items relation
        var oldOptionalItems = context.Items
                                      .Where(item => item.StockId == stock.RelatedStock.Id)
                                      .Select(item => new Item { Id = item.Id })
                                      .ToList();
        context.Items.RemoveRange(oldOptionalItems);

        // Remove them actually from the database
        context.SaveChanges();

        // Remove old items
        var oldItems = context.Items
                          .Where(item => item.StockId == stock.Id)
                          .Select(item => new Item { Id = item.Id })
                          .ToList();
        context.Items.RemoveRange(oldItems);

        context.Stocks.Update(stock);
        context.SaveChanges();         
    }
}
0 голосов
/ 02 сентября 2018

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

Это можно сделать по одному или по уровням (меньше команд SQL, но потенциально можно использовать большие списки IN PK). Связанные данные также могут быть определены с помощью SQL на основе CTE - наиболее эффективного, но независимого от базы данных способа.

Следующий метод реализует второй подход:

static void DeleteItems(DbContext context, Expression<Func<Item, bool>> filter)
{
    var items = context.Set<Item>().Where(filter).ToList();
    if (items.Count == 0) return;
    var itemIds = items.Select(e => e.Id);
    DeleteItems(context, e => e.OptionalItemId != null && itemIds.Contains(e.OptionalItemId.Value));
    context.RemoveRange(items);
}

и может использоваться в вашем коде следующим образом:

using (var context = CreateContext())
{
    // Remove old items
    DeleteItems(context, item => item.StockId == stock.Id);

    // Remove optional items from another stock
    DeleteItems(context, item => item.StockId == stock.RelatedStock.Id);

    // The rest...  
}
...