Каскадное удаление данных
в мире реляционных баз данных позволяет удалять связанные данные из зависимой таблицы, при удалении данных из основной таблицы. В случае модели, которую мы использовали в предыдущих примерах (две связанные таблицы Customer и Order), при использовании каскадного удаления, удаление данных покупателя будет вести к удалению всех связанных с ним заказов. В SQL Server и T-SQL каскадное удаление реализовано в виде опций ON DELETE CASCADE и ON UPDATE CASCADE, которые указываются при объявлении внешнего ключа таблицы.
По умолчанию Code-First включает каскадное удаление для внешних ключей, не поддерживающих значение NULL, используя соответствующий SQL-код при создании таблицы. В предыдущей статье мы описали, как указать Code-First на то, что внешний ключ должен обязательно использоваться (т.е. поддерживать ограничение NOT NULL). Давайте вспомним, как это сделать:
- Можно явно указать свойство внешнего ключа, тогда Code-First по умолчанию использует для него значение NOT NULL в базе данных. В примерах мы использовали внешние ключи CustomerId и UserId.
- Если внешний ключ не указан в классе модели, тогда Code-First автоматически генерирует его, разрешая использовать NULL. Чтобы это изменить, можно использовать атрибут Required к навигационному свойству модели.
Давайте рассмотрим пример использования каскадного удаления на примере нашего тестового проекта ASP.NET. Для этого добавим новую веб-форму CascadeDelete.aspx и добавим следующий код:
using System; using System.Linq; using System.Collections.Generic; using System.Data.Entity; using CodeFirst; namespace ProfessorWeb.EntityFramework < public partial class CascadeDelete : System.Web.UI.Page < protected void Save_Click(object sender, EventArgs e) < Database.SetInitializer( new DropCreateDatabaseIfModelChanges()); // Создать заказчика Customer customer = new Customer < FirstName = "Василий", LastName = "Пупкин", Age = 20, // Добавим заказы для этого покупателя Orders = new List< new Order < ProductName = "Товар 1", Quantity = 4, PurchaseDate = DateTime.Now >, new Order < ProductName = "Товар 2", Quantity = 2, PurchaseDate = DateTime.Now >, new Order < ProductName = "Товар 3", Quantity = 5, PurchaseDate = DateTime.Now >, > >; // Вставить заказчика в базу данных SampleContext context = new SampleContext(); context.Customers.Add(customer); context.SaveChanges(); > protected void Delete_Click(object sender, EventArgs e) < SampleContext context = new SampleContext(); // Извлечь нужного покупателя из таблицы вместе с заказами Customer customer = context.Customers .Include(c =>c.Orders) .FirstOrDefault(c => c.FirstName == "Василий"); // Удалить этого покупателя if (customer != null) < context.Customers.Remove(customer); context.SaveChanges(); >> > >
В этой форме используются две кнопки для удаления и сохранения данных. В коде обработчика Save_Click происходит создание произвольного объекта Customer с тремя связанными объектами Order, после чего эти данные вставляются в базу. В коде обработчика Delete_Click мы сначала извлекаем данные нужного заказчика из базы данных, а затем удаляем его. Обратите внимание, что здесь используется «жадная загрузка» (eager loading), т.к. мы вызываем метод Include(). Это означает, что помимо данных покупателя, будут извлечены все данные связанных с ним заказов. Фактически каскадное удаление в данном случае не нужно, т.к. мы уже извлекли все связанные заказы.
Модель данных на текущий момент выглядит следующим образом:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace CodeFirst < public class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >public string LastName < get; set; >public string Email < get; set; >public int Age < get; set; >public byte[] Photo < get; set; >public List Orders < get; set; >> public class Order < public int OrderId < get; set; >public string ProductName < get; set; >public string Description < get; set; >public int Quantity < get; set; >public DateTime PurchaseDate < get; set; >[ForeignKey("Customer")] public int UserId < get; set; >public Customer Customer < get; set; >> >
Запустите наш пример и откройте в браузере веб-форму CascadeDelete.aspx и щелкните по кнопке “Сохранить”. Entity Framework воссоздаст базу данных (если модель изменилась) и добавит новые данные в таблицы Customers и Orders. Чтобы в этом убедиться, используйте средства Visual Studio или SQL Server Management Studio для просмотра данных:

Нажмите на кнопку “Удалить”, чтобы убедиться, что данные покупателя и связанные с ним заказы удаляются корректно. При этом Entity Framework отправит четыре запроса DELETE базе данных (три для каждого заказа и один для покупателя). Давайте теперь отключим использование жадной загрузки и явно используем каскадное удаление. Ниже показан измененный код обработчика Delete_Click:
// . protected void Delete_Click(object sender, EventArgs e) < SampleContext context = new SampleContext(); // Извлечь нужного покупателя из таблицы вместе с заказами Customer customer = context.Customers .FirstOrDefault(c =>c.FirstName == "Василий"); // Удалить этого покупателя if (customer != null) < context.Customers.Remove(customer); context.SaveChanges(); >>
Здесь мы удалили вызов метода Include() и теперь Code-First не известно о связанных с покупателем заказов. В отличие от предыдущего примера, здесь Entity Framework отправит один запрос DELETE для удаления покупателя. При выполнении этого запроса сработает средство каскадного удаления и SQL Server найдет связанные заказы, удалит сначала их, а уже потом удалит покупателя.
Отключение каскадного удаления данных
Возможно вам понадобиться отключить использование каскадного удаления в базе данных. Как описывалось выше, чтобы сделать это, можно удалить явное определение первичного ключа из класса модели и положиться на автоматическую генерацию первичного ключа с помощью Code-First (при этом Code-First указывает поддержку NULL для этого ключа). Также можно воспользоваться средствами Fluent API для явного отключения каскадного удаления, если, например, требуется сохранить объявление первичного ключа в классе модели.
Важно помнить, что при отключении каскадного удаления в вашем приложении могут возникать ошибки, если вы не позаботитесь об извлечении связанных данных перед удалением, как мы это делали в первом примере с использованием “жадной загрузки”.
Отключить или включить каскадное удаление в Fluent API позволяет метод WillCascadeOnDelete(), которому передается логический параметр. Использование этого метода показано в примере ниже:
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity() .HasMany(c => c.Orders) .WithRequired(o => o.Customer) .WillCascadeOnDelete(false); >
Если вы запустите приложение и попробуете удалить данные, используя второй пример обработчика Delete_Click, то возникнет исключение, показанное на рисунке ниже:

Как уже описывалось ранее, при удалении данных из родительской таблицы, необходимо позаботиться об удалении данных из производной таблицы. Мы забыли извлечь данные связанных заказов из таблицы Orders и поэтому SQL Server вернул ошибку при попытке удаления данных только покупателя. Если вы теперь включите “жадную загрузку” с помощью метода Include() в обработчике Delete_Click, то эта ошибка исчезнет, но возникнет новая – как описывалось выше, в этом случае Code-First отправит четыре запроса на удаление и при удалении первого заказа Code-First установит для свойства Order.Customer значение NULL, а т.к. наша модель содержит внешний ключ, который не может иметь значение NULL возникнет ошибка.
Из этого описания можно сделать вывод, что для данного примера отключение каскадного удаления нельзя применить, но тогда возникает вопрос, зачем вообще отменять каскадное удаление? По своему опыту скажу, что отключение каскадного удаления используется в основном при получении циклической ссылки между таблицами в сложных базах данных. Такая ссылка может возникнуть, если между несколькими таблицами используется отношение “родительская-дочерняя” и последняя зависимая таблица неожиданно ссылается на одну из родительских таблиц. Проблема циклических ссылок проявляется не только при удалении данных, а также при их обновлении (операция UPDATE в T-SQL).
Также отключение каскадного удаления требуется для таблиц, которые определяют несколько отношений между собой. Некоторые базы данных (в том числе SQL Server) не поддерживают несколько отношений, которые определяют каскадное удаление, указываемое на одной таблице.
Каскадное удаление
В Entity Framework Core (EF Core) связи представлены с помощью внешних ключей. Сущность с внешним ключом является дочерней или зависимой в связи. Значение внешнего ключа этой сущности должно соответствовать значению первичного ключа (или альтернативному значению ключа) связанной основной или родительской сущности.
Если основная или родительская сущность удалена, значения внешних ключей зависимых и дочерних сущностей больше не соответствуют первичному или альтернативному ключу какой-либо основной или родительской сущности. Это недопустимое состояние, которое в большинстве баз данных приведет к нарушению ссылочного ограничения.
Избежать этого нарушения можно двумя способами.
- Присвоить внешним ключам значения NULL.
- Удалить соответствующие зависимые или дочерние сущности.
Первый способ подходит только для необязательных связей, в которых свойство внешнего ключа (и столбец базы данных, с которым оно сопоставлено), должно допускать значения NULL.
Второй вариант допустим для любого типа связи и называется каскадным удалением.
В этом документе описывается каскадное удаление (и удаление потерянных объектов) в контексте обновления базы данных. При этом активно используются концепции, представленные в разделах Отслеживание изменений в EF Core и Изменение внешних ключей и свойств навигации. Прежде чем приступать к изучению этого документа, обязательно ознакомьтесь с ними.
Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.
Когда происходит каскадная реакция на события
Каскадное удаление требуется в тех случаях, когда зависимая или дочерняя сущность теряет связь со своей текущей основной или родительской сущностью. Это может произойти в результате удаления основной или родительской сущности, а также в случае разрыва связи между ней и зависимой или дочерней сущностью.
Удаление основной или родительской сущности
Рассмотрим простую модель, в которой Blog является основной или родительской сущностью в связи с дочерней или зависимой сущностью Post . В качестве внешнего ключа используется свойство Post.BlogId , значение которого должно соответствовать первичному ключу Blog.Id блога, которому принадлежит запись.
public class Blog < public int Id < get; set; >public string Name < get; set; >public IList Posts < get; >= new List(); > public class Post < public int Id < get; set; >public string Title < get; set; >public string Content < get; set; >public int BlogId < get; set; >public Blog Blog < get; set; >>
По соглашению эта связь настроена как обязательная, поскольку свойство внешнего ключа Post.BlogId не допускает значения NULL. Для обязательных связей по умолчанию используется каскадное удаление. Дополнительные сведения о моделировании связей см. в разделе Связи.
При удалении блога происходит каскадное удаление всех его записей. Например:
e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges();
Метод SaveChanges создает следующий код SQL (на примере SQL Server).
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Posts] WHERE [Id] = @p0; SELECT @@ROWCOUNT; -- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Posts] WHERE [Id] = @p0; SELECT @@ROWCOUNT; -- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Blogs] WHERE [Id] = @p1; SELECT @@ROWCOUNT;
Разрыв связи
Вместо удаления блога можно разорвать связь между ним и каждой его записью. Это можно сделать, установив значение NULL для свойства навигации по ссылке Post.Blog для каждой записи:
e.Name).Include(e => e.Posts).First(); foreach (var post in blog.Posts) < post.Blog = null; >context.SaveChanges(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => 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(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First(); blog.Posts.Clear(); context.SaveChanges();
В обоих случаях будет достигнут одинаковый результат, то есть записи, которые больше не связаны ни с одним блогом, удаляются, а сам блог остается.
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Posts] WHERE [Id] = @p0; SELECT @@ROWCOUNT; -- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Posts] WHERE [Id] = @p0; SELECT @@ROWCOUNT;
Удаление сущностей, которые больше не связаны ни с одной основной или родительской сущностью, называется удалением потерянных объектов.
Концепции каскадного удаления и удаления потерянных объектов тесно взаимосвязаны. В обоих случаях при разрыве связи с основной или родительской сущностью ее зависимые и дочерние сущности удаляются. При каскадном удалении это происходит вследствие удаления самой основной или родительской сущности. При удалении потерянных объектов основная или родительская сущность по-прежнему существует, но при этом больше не связана с зависимыми и дочерними сущностями.
Где происходит каскадная реакция на события
Каскадная реакция на события может применяться к следующим сущностям.
- Сущности, которые отслеживаются текущим экземпляром DbContext.
- Сущности в базе данных, которые не были загружены в контекст.
Каскадное удаление отслеживаемых сущностей
EF Core всегда применяет к отслеживаемым сущностям каскадную реакцию на события. Это значит, что, если приложение загружает все соответствующие зависимые или дочерние сущности в DbContext (как показано в приведенных выше примерах), каскадная реакция на события будет корректно применяться, независимо конфигурации базы данных.
Точное время, когда происходит каскадная реакция для отслеживаемых сущностей, можно контролировать с помощью свойств ChangeTracker.CascadeDeleteTiming и ChangeTracker.DeleteOrphansTiming. Дополнительные сведения см. в разделе Изменение внешних ключей и свойств навигации.
Каскадное удаление в базе данных
Многие системы баз данных также реализуют каскадную реакцию на события, которая активируется при удалении сущности в базе данных. EF Core определяет конфигурацию такой реакции на основе настроек каскадного удаления в модели EF Core при создании базы данных с использованием метода EnsureCreated или миграций EF Core. Например, в представленной выше модели при использовании SQL Server для записей создается следующая таблица.
CREATE TABLE [Posts] ( [Id] int NOT NULL IDENTITY, [Title] nvarchar(max) NULL, [Content] nvarchar(max) NULL, [BlogId] int NOT NULL, CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]), CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) ON DELETE CASCADE );
Обратите внимание, что ограничение внешнего ключа, определяющее связь между блогами и записями, настраивается с использованием ON DELETE CASCADE .
Если мы знаем, что база данных настроена таким образом, можно удалить блог без предварительной загрузки записей. В этом случае база данных автоматически удалит все связанные с ним записи. Например:
e.Name).First(); context.Remove(blog); context.SaveChanges(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => e.Name).First(); context.Remove(blog); context.SaveChanges();
Обратите внимание, что для записей не используется Include , поэтому они не загружаются. В этом случае метод SaveChanges удаляет только блог, поскольку это единственная отслеживаемая сущность.
-- Executed DbCommand (6ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Blogs] WHERE [Id] = @p0; SELECT @@ROWCOUNT;
Если ограничение внешнего ключа в базе данных не настроено для каскадного удаления, это приведет к возникновению исключения. Тем не менее в этом случае записи будут удалены базой данных, так как при ее создании была задана настройка ON DELETE CASCADE .
Как правило, базы данных не поддерживают автоматическое удаление потерянных объектов. Это связано с тем, что в базах данных используются только внешние ключи и нет свойств навигации, тогда как в EF Core связи представляются с использованием и внешних ключей, и свойств навигации. Это значит, что в большинстве случаев для разрыва связи обе ее стороны должны быть загружены в DbContext.
Выполняющаяся в памяти база данных EF Core в настоящее время не поддерживает каскадное удаление в базе данных.
Не следует настраивать каскадное удаление в базе данных при обратимом удалении сущностей. Это может привести к случайному полному (вместо обратимого) удалению сущностей.
Ограничения для каскадной реакции на события в базе данных
Некоторые базы данных, особенно SQL Server, устанавливают ограничения для каскадных операций, которые приводят к возникновению циклов. Рассмотрим следующую модель.
public class Blog < public int Id < get; set; >public string Name < get; set; >public IList Posts < get; >= new List(); public int OwnerId < get; set; >public Person Owner < get; set; >> public class Post < public int Id < get; set; >public string Title < get; set; >public string Content < get; set; >public int BlogId < get; set; >public Blog Blog < get; set; >public int AuthorId < get; set; >public Person Author < get; set; >> public class Person < public int Id < get; set; >public string Name < get; set; >public IList Posts < get; >= new List(); public Blog OwnedBlog < get; set; >>
В этой модели устанавливаются три связи, каждая из которых является обязательной и по соглашению настраивается для каскадного удаления.
- При удалении блога происходит каскадное удаление всех связанных с ним записей.
- При удалении автора записей происходит каскадное удаление созданных им записей.
- При удалении владельца блога происходит каскадное удаление самого блога.
Это вполне разумный, хотя и достаточно строгий подход к управлению блогом. Тем не менее при попытке создать базу данных SQL Server с этими настройками каскадной реакции на событий возникнет следующее исключение.
Microsoft.Data.SqlClient.SqlException (0x80131904): введение ограничения FOREIGN KEY «FK_Posts_Person_AuthorId» в таблице «Записи» может вызвать циклы или несколько каскадных путей. Укажите ON DELETE NO ACTION или ON UPDATE NO ACTION либо измените другие ограничения внешнего ключа (FOREIGN KEY).
Эту проблему можно решить двумя способами.
- Изменить одну связь или несколько, отключив их каскадное удаление.
- Настроить базу данных, отключив одно или несколько из этих действий каскадного удаления, после чего убедиться, что все зависимые сущности загружены, чтобы позволить EF Core реализовывать каскадную реакцию на события.
Применение первого подхода в нашем примере позволяет сделать связь между блогом и его владельцем необязательной, предоставив свойство внешнего ключа, допускающее значение NULL.
public int? BlogId
Поскольку эта связь не является обязательной, блог может существовать без владельца, то есть каскадное удаление больше не будет настроено по умолчанию. Это значит, что каскадная реакция на события больше не будет приводить к возникновению циклов, благодаря чему база данных может быть создана в SQL Server без ошибок.
Если использовать второй подход, связь между блогом и владельцем можно оставить обязательной и настроить для каскадного удаления, но при этом задать применение этой конфигурации только к отслеживаемым сущностям, а не к базе данных.
() .HasOne(e => e.Owner) .WithOne(e => e.OwnedBlog) .OnDelete(DeleteBehavior.ClientCascade); > —>
protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder .Entity() .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(); —>
using var context = new BlogsContext(); var owner = context.People.Single(e => e.Name == "ajcvickers"); var blog = context.Blogs.Single(e => e.Owner == owner); context.Remove(owner); context.SaveChanges();
EF Core выполнит каскадное удаление владельца и принадлежащего ему блога.
-- Executed DbCommand (8ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Blogs] WHERE [Id] = @p0; SELECT @@ROWCOUNT; -- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [People] WHERE [Id] = @p1; SELECT @@ROWCOUNT;
Тем не менее, если блог не был загружен, при удалении его владельца происходит следующее.
e.Name == «ajcvickers»); context.Remove(owner); context.SaveChanges(); —>
using var context = new BlogsContext(); var owner = context.People.Single(e => e.Name == "ajcvickers"); context.Remove(owner); context.SaveChanges();
После этого возникает исключение из-за нарушения ограничения внешнего ключа в базе данных.
Microsoft.Data.SqlClient.SqlException: инструкция DELETE конфликтует с ограничением REFERENCE «FK_Blogs_Люди_OwnerId». Конфликт произошел в базе данных Scratch, таблица dbo.Blogs, столбец OwnerId. Выполнение данной инструкции было прервано.
Каскадное распространение значений NULL
Для необязательных связей свойства внешнего ключа, допускающие значения NULL, сопоставлены со столбцами базы данных, также допускающими такие значения. Это означает, что внешнему ключу может быть присвоено значение NULL, если текущая основная или родительская сущность удаляется или разрывается ее связь с зависимой или дочерней сущностью.
Еще раз возьмем примеры из раздела Когда происходит каскадная реакция на события. На этот раз рассмотрим необязательную связь, которая представлена допускающим значение NULL свойством внешнего ключа Post.BlogId :
public int? BlogId
Этому свойству внешнего ключа будет присваиваться значение NULL для каждой записи при удалении связанного с ней блога. Например, следующий код аналогичен приведенному ранее.
e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First(); context.Remove(blog); context.SaveChanges();
Тем не менее теперь при вызове метода SaveChanges будут выполнены следующие обновления базы данных.
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Posts] SET [BlogId] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT; -- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Posts] SET [BlogId] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT; -- Executed DbCommand (1ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; DELETE FROM [Blogs] WHERE [Id] = @p2; SELECT @@ROWCOUNT;
Аналогичным образом, при разрыве связи в любом из приведенных выше примеров:
e.Name).Include(e => e.Posts).First(); foreach (var post in blog.Posts) < post.Blog = null; >context.SaveChanges(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => 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(); —>
using var context = new BlogsContext(); var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First(); blog.Posts.Clear(); context.SaveChanges();
При вызове метода SaveChanges записи будут обновлены с присвоением внешнему ключу значения NULL.
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Posts] SET [BlogId] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT; -- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [Posts] SET [BlogId] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT;
Дополнительные сведения о том, как 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); > —>
protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder .Entity() .HasOne(e => e.Owner) .WithOne(e => e.OwnedBlog) .OnDelete(DeleteBehavior.ClientCascade); >
Дополнительные сведения о настройке связей между типами сущностей см. в разделе Связи.
Метод OnDelete принимает значение из перечисления DeleteBehavior, с использованием которого связаны определенные сложности. В этом перечислении определяется поведение EF Core для отслеживаемых сущностей и задается конфигурация каскадного удаления в базе данных в случаях, когда для создания схемы используется EF.
Влияние на схему базы данных
В следующей таблице показан результат применения каждого значения метода OnDelete для ограничения внешнего ключа, создаваемого с помощью миграций EF Core или метода EnsureCreated.
| Значение перечисления DeleteBehavior | Влияние на схему базы данных |
|---|---|
| Cascade | ПРИ УДАЛЕНИИ КАСКАДА |
| Ограничить | ON DELETE RESTRICT |
| NoAction | database default |
| SetNull | ПРИ УДАЛЕНИИ НАБОРА 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
В следующих разделах приводятся таблицы, в которых описывается, что происходит с зависимыми или дочерними сущностями при удалении основной или родительской сущности, а также при разрыве ее связи с зависимой или дочерней сущностью. Каждая таблица описывает одну из следующих ситуаций.
- Необязательные (внешний ключ, допускающий значения NULL) и обязательные (не допускающий значения NULL внешний ключ) связи
- Зависимые или дочерние сущности загружаются в DbContext и отслеживаются в нем (либо существуют только в базе данных).
Обязательная связь с загруженными зависимыми или дочерними сущностями
| Значение перечисления DeleteBehavior | Поведение при удалении основной или родительской сущности | Поведение при разрыве связи с основной или родительской сущностью |
|---|---|---|
| Cascade | EF Core удаляет зависимые сущности | EF Core удаляет зависимые сущности |
| Ограничить | InvalidOperationException | InvalidOperationException |
| NoAction | InvalidOperationException | InvalidOperationException |
| SetNull | При создании базы данных возникает исключение SqlException | При создании базы данных возникает исключение SqlException |
| ClientSetNull | InvalidOperationException | InvalidOperationException |
| ClientCascade | EF Core удаляет зависимые сущности | EF Core удаляет зависимые сущности |
| ClientNoAction | DbUpdateException | InvalidOperationException |
- По умолчанию для таких обязательных связей задается значение Cascade .
- Применение любого другого значения для каскадного удаления обязательных связей приведет к возникновению исключения при вызове метода SaveChanges.
- Как правило, в этом случае возникает исключение EF Core InvalidOperationException , так как в загруженных дочерних или зависимых сущностях обнаруживается недопустимое состояние.
- Если используется значение ClientNoAction , EF Core не проверяет зависимые сущности адресной привязки перед их отправкой в базу данных, поэтому в этом случае база данных создает исключение, которое затем упаковывается в исключение DbUpdateException методом SaveChanges.
- Значение SetNull при создании базы данных отклоняется, поскольку столбец внешнего ключа не допускает значения NULL.
Обязательная связь, для которой не загружены зависимые или дочерние сущности
Значение перечисления DeleteBehavior Поведение при удалении основной или родительской сущности Поведение при разрыве связи с основной или родительской сущностью Cascade База данных удаляет зависимые сущности Неприменимо Ограничить DbUpdateException Неприменимо NoAction DbUpdateException Неприменимо SetNull При создании базы данных возникает исключение SqlException Неприменимо ClientSetNull DbUpdateException Неприменимо ClientCascade DbUpdateException Неприменимо ClientNoAction DbUpdateException Неприменимо - Разрыв связи в этом случае невозможен, поскольку зависимые или дочерние сущности не загружаются.
- По умолчанию для таких обязательных связей задается значение Cascade .
- Применение любого другого значения для каскадного удаления обязательных связей приведет к возникновению исключения при вызове метода SaveChanges.
- Как правило, это исключение DbUpdateException , поскольку зависимые или дочерние сущности не загружаются, поэтому недопустимое состояние может быть обнаружено только базой данных. После этого метод SaveChanges упаковывает исключение базы данных в DbUpdateException .
- Значение SetNull при создании базы данных отклоняется, поскольку столбец внешнего ключа не допускает значения NULL.
Необязательная связь с загруженными зависимыми или дочерними сущностями
Значение перечисления DeleteBehavior Поведение при удалении основной или родительской сущности Поведение при разрыве связи с основной или родительской сущностью Cascade EF Core удаляет зависимые сущности EF Core удаляет зависимые сущности Ограничить EF Core присваивает значения NULL внешним ключам зависимых сущностей EF Core присваивает значения NULL внешним ключам зависимых сущностей NoAction EF Core присваивает значения NULL внешним ключам зависимых сущностей EF Core присваивает значения NULL внешним ключам зависимых сущностей SetNull EF Core присваивает значения NULL внешним ключам зависимых сущностей EF Core присваивает значения NULL внешним ключам зависимых сущностей ClientSetNull EF Core присваивает значения NULL внешним ключам зависимых сущностей EF Core присваивает значения NULL внешним ключам зависимых сущностей ClientCascade EF Core удаляет зависимые сущности EF Core удаляет зависимые сущности ClientNoAction DbUpdateException EF Core присваивает значения NULL внешним ключам зависимых сущностей - По умолчанию для таких необязательных связей задается значение ClientSetNull .
- Зависимые и дочерние сущности никогда не удаляются, если не задано значение Cascade или ClientCascade .
- Все остальные значения приводят к тому, что зависимые FK будут иметь значение NULL в EF Core.
- Исключением является значение ClientNoAction , при котором EF Core не затрагивает внешние ключи зависимых или дочерних сущностей при удалении основной или родительской сущности. Таким образом, база данных создает исключение, которое упаковывается в DbUpdateException методом SaveChanges.
Необязательная связь, для которой не загружены зависимые или дочерние сущности
Значение перечисления DeleteBehavior Поведение при удалении основной или родительской сущности Поведение при разрыве связи с основной или родительской сущностью Cascade База данных удаляет зависимые сущности Неприменимо Ограничить DbUpdateException Неприменимо NoAction DbUpdateException Неприменимо SetNull База данных присваивает значения NULL внешним ключам зависимых сущностей Неприменимо ClientSetNull DbUpdateException Неприменимо ClientCascade DbUpdateException Неприменимо ClientNoAction DbUpdateException Неприменимо - Разрыв связи в этом случае невозможен, поскольку зависимые или дочерние сущности не загружаются.
- По умолчанию для таких необязательных связей задается значение ClientSetNull .
- Чтобы избежать возникновения исключения базы данных, зависимые и дочерние сущности должны загружаться, если только база данных не настроена для каскадного удаления или распространения значений NULL.
Как включить каскадное удаление sql server
Внешние ключи применяются для установки связи между таблицами. Внешний ключ устанавливается для столбцов из зависимой, подчиненной таблицы, и указывает на один из столбцов из главной таблицы. Хотя, как правило, внешний ключ указывает на первичный ключ из связанной главной таблицы, но это необязательно должно быть непременным условием. Внешний ключ также может указывать на какой-то другой столбец, который имеет уникальное значение.
Общий синтаксис установки внешнего ключа на уровне столбца:
[FOREIGN KEY] REFERENCES главная_таблица (столбец_главной_таблицы) [ON DELETE ] [ON UPDATE ]
Для создания ограничения внешнего ключа на уровне столбца после ключевого слова REFERENCES указывается имя связанной таблицы и в круглых скобках имя связанного столбца, на который будет указывать внешний ключ. Также обычно добавляются ключевые слова FOREIGN KEY , но в принципе их необязательно указывать. После выражения REFERENCES идет выражение ON DELETE и ON UPDATE .
Общий синтаксис установки внешнего ключа на уровне таблицы:
FOREIGN KEY (стобец1, столбец2, . столбецN) REFERENCES главная_таблица (столбец_главной_таблицы1, столбец_главной_таблицы2, . столбец_главной_таблицыN) [ON DELETE ] [ON UPDATE ]
Например, определим две таблицы и свяжем их посредством внешнего ключа:
CREATE TABLE Customers ( Id INT PRIMARY KEY IDENTITY, Age INT DEFAULT 18, FirstName NVARCHAR(20) NOT NULL, LastName NVARCHAR(20) NOT NULL, Email VARCHAR(30) UNIQUE, Phone VARCHAR(20) UNIQUE ); CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT REFERENCES Customers (Id), CreatedAt Date );
Здесь определены таблицы Customers и Orders. Customers является главной и представляет клиента. Orders является зависимой и представляет заказ, сделанный клиентом. Эта таблица через столбец CustomerId связана с таблицей Customers и ее столбцом Id. То есть столбец CustomerId является внешним ключом, который указывает на столбец Id из таблицы Customers.
Определение внешнего ключа на уровне таблицы выглядело бы следующим образом:
CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt Date, FOREIGN KEY (CustomerId) REFERENCES Customers (Id) );
С помощью оператора CONSTRAINT можно задать имя для ограничения внешнего ключа. Обычно это имя начинается с префикса «FK_»:
CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt Date, CONSTRAINT FK_Orders_To_Customers FOREIGN KEY (CustomerId) REFERENCES Customers (Id) );
В данном случае ограничение внешнего ключа CustomerId называется «FK_Orders_To_Customers».
ON DELETE и ON UPDATE
С помощью выражений ON DELETE и ON UPDATE можно установить действия, которые выполняться соответственно при удалении и изменении связанной строки из главной таблицы. И для определения действия мы можем использовать следующие опции:
- CASCADE : автоматически удаляет или изменяет строки из зависимой таблицы при удалении или изменении связанных строк в главной таблице.
- NO ACTION : предотвращает какие-либо действия в зависимой таблице при удалении или изменении связанных строк в главной таблице. То есть фактически какие-либо действия отсутствуют.
- SET NULL : при удалении связанной строки из главной таблицы устанавливает для столбца внешнего ключа значение NULL.
- SET DEFAULT : при удалении связанной строки из главной таблицы устанавливает для столбца внешнего ключа значение по умолчанию, которое задается с помощью атрибуты DEFAULT. Если для столбца не задано значение по умолчанию, то в качестве него применяется значение NULL.
Каскадное удаление
По умолчанию, если на строку из главной таблицы по внешнему ключу ссылается какая-либо строка из зависимой таблицы, то мы не сможем удалить эту строку из главной таблицы. Вначале нам необходимо будет удалить все связанные строки из зависимой таблицы. И если при удалении строки из главной таблицы необходимо, чтобы были удалены все связанные строки из зависимой таблицы, то применяется каскадное удаление, то есть опция CASCADE :
CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt Date, FOREIGN KEY (CustomerId) REFERENCES Customers (Id) ON DELETE CASCADE )
Аналогично работает выражение ON UPDATE CASCADE . При изменении значения первичного ключа автоматически изменится значение связанного с ним внешнего ключа. Но так как первичные ключи, как правило, изменяются очень редко, да и с принципе не рекомендуется использовать в качестве первичных ключей столбцы с изменяемыми значениями, то на практике выражение ON UPDATE используется редко.
Установка NULL
При установки для внешнего ключа опции SET NULL необходимо, чтобы столбец внешнего ключа допускал значение NULL:
CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt Date, FOREIGN KEY (CustomerId) REFERENCES Customers (Id) ON DELETE SET NULL );
Установка значения по умолчанию
CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt Date, FOREIGN KEY (CustomerId) REFERENCES Customers (Id) ON DELETE SET DEFAULT )
Как включить каскадное удаление sql server
Внешний ключ (Foreign Key) это связь с другой таблицей.
Чтобы образовать внешний ключ нужно чтобы
Имя_свойства_в_нашем_классе
=
Имя_класса_связанной_таблицы + Имя_ключа_из_связанной_таблицыКаскадное удаление
• Если внешний ключ определяется как int? то внешний ключ может иметь null значения и нет каскадного удаления .
• Если внешний ключ определяется как int то внешний ключ не может иметь null значения и будет каскадное удаление .Пример1 (foreign key может быть null)
public class UserContext : DbContext
<
// MyConnection1 это соединение с базой данных описанное в файле web.config
public UserContext() : base( «MyConnection1» )
<
>public class User
<
public int Id
public string Email
public string Password
>public class Task
<
public int Id < get; set; >
public int Pricepublic int? UserId < get; set; >// определяем внешний ключ (int? значит может иметь null значение)
public User User < get; set; >// это тоже необходимо для внешнего ключа
>На заметку!
Если внешний ключ определяется как int? то внешний ключ может иметь null значения и нет каскадного удаления .
В MS Sql Server база данных выглядит так:
Пример2 (foreign key не может быть null)
public class UserContext : DbContext
<
// MyConnection1 это соединение с базой данных описанное в файле web.config
public UserContext() : base( «MyConnection1» )
<
>public class User
<
public int Id
public string Email
public string Password
>public class Task
<
public int Id < get; set; >
public int Pricepublic int UserId < get; set; >// определяем внешний ключ (int значит НЕ может иметь null значение)
public User User < get; set; >// это тоже необходимо для внешнего ключа
>На заметку!
Если внешний ключ определяется как int то внешний ключ НЕ может иметь null значения и будет каскадное удаление .
В MS Sql Server база данных выглядит так:
