Каскадное удаление entity framework

Обновлено: 11.05.2024

У меня проблема с удалением записи с EntityFramework и отношениями "многие ко многим" для одного и того же объекта. Рассмотрим этот простой пример:

Конфигурация Fluent API:

    Правильно ли, что невозможно определить Cascade Delete в Fluent API?

Каков наилучший способ удаления UserEntity , например Foo ?

Теперь он ищет меня, мне нужно Clear коллекцию Foo Friends , тогда мне нужно загрузить все остальные UserEntities , которые содержат Foo в Friends , а затем удалить Foo из каждого списка, прежде чем я удалю Foo из Users . Но это звучит слишком сложно.

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

Update01:

Мое лучшее решение для этой проблемы для ноу-хау - это просто выполнить команду raw sql перед вызовом SaveChanges :

Но недостатком этого является то, что если SaveChanges по какой-то причине не удается, FriendshipRelation уже удалены и не могут быть отброшены. Или я не прав?

Проблема 1

Ответ довольно прост:

Entity Framework не может определить каскадное удаление, когда он не знает, какие свойства принадлежат отношению.

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

Решение создает объект FriendshipRelation . Вот так:

Теперь вам нужно изменить UserEntity . Вместо набора UserEntity он имеет набор UserFriendship . Вот так:

Посмотрим на отображение:

Чтобы получить всех друзей пользователя:

Все это прекрасно работает. Однако есть проблема в этом сопоставлении (что также происходит в вашем текущем сопоставлении). Предположим, что "I" есть a UserEntity :

    Я сделал запрос друга Джону - Джон принял
    Я попросил друга обратиться к Энн-Энн.
    Ричард обратился ко мне с другом - я принял

Когда я получаю свойство Friends , он возвращает "Джон", "Энн", но не "Ричард". Зачем? потому что Ричард - "создатель" отношений, а не я. Свойство Friends привязано только к одной стороне отношения.

Ok. Как я могу это решить? Легко! Измените класс UserEntity :

Не требуется никаких миграций.

Чтобы получить всех друзей пользователя:

Проблема 2

Да, вам нужно повторить сборку и удалить все дочерние объекты. См. Мой ответ в этой теме Чистое обновление иерархии в Entity Framework

Следуя моему ответу, просто создайте UserFriendship dbset:

Теперь вы можете получить всех друзей определенного идентификатора пользователя, просто удалить все из них одним выстрелом, а затем удалить пользователя.

Проблема 3

Да, это возможно. Теперь у вас есть dbset UserFriendship .

Надеюсь, что это поможет!

1) Я не вижу прямого способа управления каскадом для отношений "многие ко многим" с использованием FluentApi.

2) Единственный доступный способ, которым я могу управлять, это использовать ManyToManyCascadeDeleteConvention , который, я думаю, включен по умолчанию, по крайней мере, для меня. Я только что проверил одну из моих миграций, включая отношения "многие ко многим", и, действительно, cascadeDelete: true существует для обоих ключей.

EDIT: Извините, я просто обнаружил, что ManyToManyCascadeDeleteConvention не распространяется на случай саморегуляции. В этом ответном ответе говорится, что

Итак, вам нужно иметь собственный код удаления (например, команду sql, которую у вас уже есть) и выполнить его в области транзакций.

3) Вы не сможете получить доступ к этой таблице из контекста. Обычно таблица, созданная отношением "многие ко многим", является побочным продуктом реализации в реляционной СУБД и считается слабой таблицей, соответствующей связанным с ней таблицам, что означает, что ее строки должны быть каскадно-удалены, если один из связанных объектов.

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

4) Чтобы сделать это, если вы действительно хотите (FluentApi включает по умолчанию ManyToManyCascadeDeleteConvention ), заключить команду sql и ваши SaveChanges в область транзакции.

В Entity Framework Core (EF Core) связи представлены с помощью внешних ключей. Сущность с внешним ключом является дочерней или зависимой в связи. Значение внешнего ключа этой сущности должно соответствовать значению первичного ключа (или альтернативному значению ключа) связанной основной или родительской сущности.

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

Избежать этого нарушения можно двумя способами.

  1. Присвоить внешним ключам значения NULL.
  2. Удалить соответствующие зависимые или дочерние сущности.

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

Второй вариант допустим для любого типа связи и называется каскадным удалением.

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

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Когда происходит каскадная реакция на события

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

Удаление основной или родительской сущности

Рассмотрим простую модель, в которой Blog является основной или родительской сущностью в связи с дочерней или зависимой сущностью Post . В качестве внешнего ключа используется свойство Post.BlogId , значение которого должно соответствовать первичному ключу Blog.Id блога, которому принадлежит запись.

По соглашению эта связь настроена как обязательная, поскольку свойство внешнего ключа Post.BlogId не допускает значения NULL. Для обязательных связей по умолчанию используется каскадное удаление. Дополнительные сведения о моделировании связей см. в разделе Связи.

При удалении блога происходит каскадное удаление всех его записей. Пример:

e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges(); -->

Метод SaveChanges создает следующий код SQL (на примере SQL Server).

Разрыв связи

Вместо удаления блога можно разорвать связь между ним и каждой его записью. Это можно сделать, установив значение NULL для свойства навигации по ссылке Post.Blog для каждой записи:

e.Name).Include(e => e.Posts).First(); foreach (var post in blog.Posts) < post.Blog = null; >context.SaveChanges(); -->

Связь также можно разорвать, удалив каждую запись из свойства навигации по коллекции Blog.Posts .

e.Name).Include(e => e.Posts).First(); blog.Posts.Clear(); context.SaveChanges(); -->

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

Удаление сущностей, которые больше не связаны ни с одной основной или родительской сущностью, называется удалением потерянных объектов.

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

Где происходит каскадная реакция на события

Каскадная реакция на события может применяться к следующим сущностям.

  • Сущности, которые отслеживаются текущим экземпляром DbContext.
  • Сущности в базе данных, которые не были загружены в контекст.

Каскадное удаление отслеживаемых сущностей

EF Core всегда применяет к отслеживаемым сущностям каскадную реакцию на события. Это значит, что, если приложение загружает все соответствующие зависимые или дочерние сущности в DbContext (как показано в приведенных выше примерах), каскадная реакция на события будет корректно применяться, независимо конфигурации базы данных.

Точное время, когда происходит каскадная реакция для отслеживаемых сущностей, можно контролировать с помощью свойств ChangeTracker.CascadeDeleteTiming и ChangeTracker.DeleteOrphansTiming. Дополнительные сведения см. в разделе Изменение внешних ключей и свойств навигации.

Каскадное удаление в базе данных

Многие системы баз данных также реализуют каскадную реакцию на события, которая активируется при удалении сущности в базе данных. EF Core определяет конфигурацию такой реакции на основе настроек каскадного удаления в модели EF Core при создании базы данных с использованием метода EnsureCreated или миграций EF Core. Например, в представленной выше модели при использовании SQL Server для записей создается следующая таблица.

Обратите внимание, что ограничение внешнего ключа, определяющее связь между блогами и записями, настраивается с использованием ON DELETE CASCADE .

Если мы знаем, что база данных настроена таким образом, можно удалить блог без предварительной загрузки записей. В этом случае база данных автоматически удалит все связанные с ним записи. Пример:

e.Name).First(); context.Remove(blog); context.SaveChanges(); -->

Обратите внимание, что для записей не используется Include , поэтому они не загружаются. В этом случае метод SaveChanges удаляет только блог, поскольку это единственная отслеживаемая сущность.

Если ограничение внешнего ключа в базе данных не настроено для каскадного удаления, это приведет к возникновению исключения. Тем не менее в этом случае записи будут удалены базой данных, так как при ее создании была задана настройка ON DELETE CASCADE .

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

Выполняющаяся в памяти база данных EF Core в настоящее время не поддерживает каскадное удаление в базе данных.

Не следует настраивать каскадное удаление в базе данных при обратимом удалении сущностей. Это может привести к случайному полному (вместо обратимого) удалению сущностей.

Ограничения для каскадной реакции на события в базе данных

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

В этой модели устанавливаются три связи, каждая из которых является обязательной и по соглашению настраивается для каскадного удаления.

  • При удалении блога происходит каскадное удаление всех связанных с ним записей.
  • При удалении автора записей происходит каскадное удаление созданных им записей.
  • При удалении владельца блога происходит каскадное удаление самого блога.

Это вполне разумный, хотя и достаточно строгий подход к управлению блогом. Тем не менее при попытке создать базу данных SQL Server с этими настройками каскадной реакции на событий возникнет следующее исключение.

Microsoft.Data.SqlClient.SqlException (0x80131904): Введение ограничения внешнего ключа (FOREIGN KEY) FK_Posts_Person_AuthorId для таблицы Posts может привести к появлению циклов или множественных каскадных путей. Укажите ON DELETE NO ACTION или ON UPDATE NO ACTION либо измените другие ограничения внешнего ключа (FOREIGN KEY).

Эту проблему можно решить двумя способами.

  1. Изменить одну связь или несколько, отключив их каскадное удаление.
  2. Настроить базу данных, отключив одно или несколько из этих действий каскадного удаления, после чего убедиться, что все зависимые сущности загружены, чтобы позволить EF Core реализовывать каскадную реакцию на события.

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

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

Если использовать второй подход, связь между блогом и владельцем можно оставить обязательной и настроить для каскадного удаления, но при этом задать применение этой конфигурации только к отслеживаемым сущностям, а не к базе данных.

() .HasOne(e => e.Owner) .WithOne(e => e.OwnedBlog) .OnDelete(DeleteBehavior.ClientCascade); > -->

Что произойдет, если загрузить владельца блога и сам блог, а затем удалить владельца?

e.Name == "ajcvickers"); var blog = context.Blogs.Single(e => e.Owner == owner); context.Remove(owner); context.SaveChanges(); -->

EF Core выполнит каскадное удаление владельца и принадлежащего ему блога.

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

e.Name == "ajcvickers"); context.Remove(owner); context.SaveChanges(); -->

После этого возникает исключение из-за нарушения ограничения внешнего ключа в базе данных.

Microsoft.Data.SqlClient.SqlException: Конфликт инструкции DELETE с ограничением REFERENCE FK_Blogs_People_OwnerId. Конфликт произошел в базе данных Scratch, таблица dbo.Blogs, столбец OwnerId. Выполнение данной инструкции было прервано.

Каскадное распространение значений NULL

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

Еще раз возьмем примеры из раздела Когда происходит каскадная реакция на события. На этот раз рассмотрим необязательную связь, которая представлена допускающим значение NULL свойством внешнего ключа Post.BlogId :

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

e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges(); -->

Тем не менее теперь при вызове метода SaveChanges будут выполнены следующие обновления базы данных.

Аналогичным образом, при разрыве связи в любом из приведенных выше примеров:

e.Name).Include(e => e.Posts).First(); foreach (var post in blog.Posts) < post.Blog = null; >context.SaveChanges(); -->

Или сделайте так:

e.Name).Include(e => e.Posts).First(); blog.Posts.Clear(); context.SaveChanges(); -->

При вызове метода SaveChanges записи будут обновлены с присвоением внешнему ключу значения NULL.

Дополнительные сведения о том, как EF Core управляет внешними ключами и свойствами навигации при изменении их значений, см. в разделе Изменение внешних ключей и свойств навигации.

По умолчанию, начиная с первой версии Entity Framework 2008 года, выполнялась адресная привязка таких связей. До появления EF Core у такого подхода не было своего названия и его нельзя было изменить. Сейчас он называется ClientSetNull и описывается в следующем разделе.

Базы данных также можно настроить для подобного каскадного распространения значений NULL при удалении основной или родительской сущности в необязательной связи. Тем не менее этот подход применяется гораздо реже операций каскадного удаления в базе данных. Одновременное выполнение каскадного удаления и каскадного распространения значений NULL в базе данных SQL Server почти всегда будет приводить к возникновению циклов связей. Дополнительные сведения о настройке каскадного распространения значений NULL см. в следующем разделе.

Настройка каскадной реакции на события

Прежде чем переходить к этому разделу, ознакомьтесь с приведенными выше. Для понимания параметров конфигурации потребуется знание представленных ранее материалов.

Настройка каскадной реакции на события для отдельных связей осуществляется с помощью метода OnDelete в OnModelCreating. Пример:

() .HasOne(e => e.Owner) .WithOne(e => e.OwnedBlog) .OnDelete(DeleteBehavior.ClientCascade); > -->

Дополнительные сведения о настройке связей между типами сущностей см. в разделе Связи.

Метод OnDelete принимает значение из перечисления DeleteBehavior, с использованием которого связаны определенные сложности. В этом перечислении определяется поведение EF Core для отслеживаемых сущностей и задается конфигурация каскадного удаления в базе данных в случаях, когда для создания схемы используется EF.

Влияние на схему базы данных

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

Значение перечисления DeleteBehavior Влияние на схему базы данных
Cascade ON DELETE CASCADE
Ограничение ON DELETE RESTRICT
NoAction database default
SetNull ON DELETE SET NULL
ClientSetNull database default
ClientCascade database default
ClientNoAction database default

Действия ON DELETE NO ACTION (база данных по умолчанию) и ON DELETE RESTRICT в реляционных базах данных, как правило, идентичны или очень близки. Несмотря на название NO ACTION , оба эти параметра приводят к принудительному применению ссылочных ограничений. Единственное различие между ними заключается в том, когда база данных выполняет проверку ограничений. Сведения о конкретных различиях между параметрами ON DELETE NO ACTION и ON DELETE RESTRICT в вашей системе базы данных следует искать в документации по ней.

SQL Server не поддерживает ON DELETE RESTRICT , поэтому используйте ON DELETE NO ACTION .

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

Влияние на поведение метода SaveChanges

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

Есть таблица purchases , на нее ссылается таблица goods . Внешние ключи настроены на каскадное удаление, то есть при удалении записи из purchases , должны удаляться все goods . Это так и работает, если удалять запись вручную (не из кода, а через редактор таблиц БД). В EF я удаляю так

где сurPur - объект Purchase . В итоге EF отвязывает goods от Purchase , то есть ставить в NULL внешние ключи, после чего удаляет Purchase . Вот запросы от EF (он действительно делает так как я написал):


3,349 2 2 золотых знака 17 17 серебряных знаков 41 41 бронзовый знак @Bald56rus Ручное - это из базы (я через визуальный редактор делаю) строку выделил - удалить - все удалилось. Не ручное - это из кода приложения, как и показал. Нужно чтобы все удалялось связанное, а не отвязывалось. а нет ли у вас случайно такой строчки в контексте? modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); А вы случайно модель генерировали не до того как добавили каскадное удаление в базу? В любом случае имеет смысл снести и пересоздать маппинг для чистоты эксперимента и посмотреть прописывается ли каскадное удаление в модели.

Как правильно заметил @PetrAbdulin, я перенастроил внешние ключи на каскадное удаление уже после создания модели .edmx. Несмотря на то, что я ее несколько раз обновлял на основе БД, ничего не менялось (точнее менялось, но не все). Вот тут Microsoft пишет в заметке:

The conceptual model will be updated only for objects that are added to the database. All other changes to the conceptual model must be made manually. For information about updating the conceptual model, see Entity Data Model Tools Tasks

Концептуальная модель будет обновляться только для объектов, которые добавлены в базу. Все остальные изменения концептуальной модели должны быть сделаны вручную. Информация об обновлении концептуальной модели доступна по ссылке Entity Data Model Tools Tasks

Интересен тот факт (который может ввести в заблуждение), что в .edmx (если его открыть как .xml файл) в разделе <edmx:StorageModels> все изменяется:

А вот в разделе <edmx:ConceptualModels> это не происходит, то есть

На диаграмме классов выбирает нужную связь и в свойствах меняем OnDelete на Cascade

введите сюда описание изображения

И это нужно поделать со всеми связями в проекте, для которых настроено каскадное удаление.

Я использую EF Core 3.1.1, но я считаю, что этот вопрос относится ко всем версиям EF.

Кажется, EF имеет возможность каскадного удаления - если оно включено и если зависимые объекты загружаются в контексте.

Это то, что я хочу, однако при использовании code-first он также создает таблицы с включенным каскадным удалением в базе данных. ( ON DELETE CASCADE )

Можете ли вы включить каскадное удаление в EF и полагаться на удаление EF зависимых объектов, не включая также каскадное удаление на уровне базы данных? (Или я неправильно это понимаю?)

Причина в том, что миграция не удалась, потому что SQL не включит каскадное удаление в базе данных, потому что он обнаруживает несколько каскадных путей (даже если множественные не встречаются в схеме естественным образом)

2 ответа

На самом деле EF Core 3.0 является первой версией EF, которая добавляет такую возможность через DeleteBehavior.ClientCascade (к сожалению, пока не включен в раздел документации каскадного удаления):

Для объектов, отслеживаемых DbContext , зависимые объекты будут удалены при удалении соответствующего участника.

Если база данных была создана из модели с использованием Entity Framework Migrations или метода EnsureCreated() , то поведение в базе данных состоит в том, чтобы генерировать ошибку, если нарушено ограничение внешнего ключа.

Вкратце, все Client* способы удаления отображаются на Restrict , т. Е. Принудительное отношение FK в базе данных без каскада. Поведение клиента применяется только к объектам, отслеживаемым контекстом, поэтому убедитесь, что вы Include связали данные перед удалением (как в вашем примере).

Чтобы настроить этот параметр, вам потребуется как минимум свободный API с действительным Has + With , чтобы перейти к методу OnDelete , например,

Спасибо за комментарии / посты.

Мои классы модели EF и конфигурация правильные. Проблема заключается в том, что в моей схеме есть несколько каскадных путей, и это нормально, потому что основной элемент в этих путях никогда не удаляется (программно удаляется). В то время как в EF нет проблем с несколькими каскадными путями, SQL, безусловно, имеет, и SQL выдает ошибки при попытке обновить базу данных.

Я не хотел отключать каскадное удаление и устанавливать сложные каскадные функции в коде, просто из-за пары мест, где существует несколько каскадных путей.

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

Читайте также: