Что такое каскадное удаление
Перейти к содержимому

Что такое каскадное удаление

  • автор:

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

в мире реляционных баз данных позволяет удалять связанные данные из зависимой таблицы, при удалении данных из основной таблицы. В случае модели, которую мы использовали в предыдущих примерах (две связанные таблицы 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) не поддерживают несколько отношений, которые определяют каскадное удаление, указываемое на одной таблице.

Что такое каскадное удаление

Каскадное удаление представляет автоматическое удаление зависимой сущности после удаления главной.

По умолчанию для сущностей применяется каскадное удаление, если наличие связанной сущности обязательно. Например:

using Microsoft.EntityFrameworkCore; public class Company < public int Id < get; set; >public string? Name < get; set; >public List Users < get; set; >= new(); > public class User < public int Id < get; set; >public string? Name < get; set; >public int CompanyId < get; set; >public Company? Company < get; set; >> public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public DbSet Companies < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >>

Здесь свойство внешнего ключа имеет тип int , оно не допускает значения null и требует наличия конкретного значения — id связанного объекта Company (При этом то, что навигационное свойство Company допускает null, не имеет значения). То есть для объекта User обязательно необходимо наличия связанного объекта Company. Поэтому сгенерированная таблица Users будет иметь код:

CREATE TABLE "Users" ( "Id" INTEGER NOT NULL, "Name" TEXT, "CompanyId" INTEGER NOT NULL, CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT), CONSTRAINT "FK_Users_Companies_CompanyId" FOREIGN KEY("CompanyId") REFERENCES "Companies"("Id") ON DELETE CASCADE );

В определении внешнего ключа устанавливается каскадное удаление: ON DELETE CASCADE

Аналогичная связь будет устанавливаться, если свойство-внешний ключа отсутствует, а навигационное свойство НЕ представляет nullable-тип:

public class User < Company? company; public int Id < get; set; >public string? Name < get; set; >public Company Company < set =>company = value; get => company ?? throw new InvalidOperationException("Uninitialized property: Company"); > >

Такая же таблица создается, если навигационное свойство представляет nullable-тип, но оно определено как обязательное, например, с помощью атрибута Required :

using System.ComponentModel.DataAnnotations; public class User < public int Id < get; set; >public string? Name < get; set; >[Required] public Company? Company < get; set; >>

Например, добавим в базу данных 2 компании и 4 связанных с ними пользователей и затем удалим одну из компаний:

using (ApplicationContext db = new ApplicationContext()) < // добавляем начальные данные Company microsoft = new Company < Name = "Microsoft" >; Company google = new Company < Name = "Google" >; db.Companies.AddRange(microsoft, google); db.SaveChanges(); User tom = new User < Name = "Tom", Company = microsoft >; User bob = new User < Name = "Bob", Company = google >; User alice = new User < Name = "Alice", Company = microsoft >; User kate = new User < Name = "Kate", Company = google >; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); // получаем пользователей var users = db.Users.ToList(); foreach (var user in users) Console.WriteLine(user.Name); // Удаляем первую компанию var comp = db.Companies.FirstOrDefault(); if(comp!=null) db.Companies.Remove(comp); db.SaveChanges(); Console.WriteLine("\nСписок пользователей после удаления компании"); // снова получаем пользователей users = db.Users.ToList(); foreach (var user in users) Console.WriteLine(user.Name); >

Консольный вывод программы:

Bob Tom Alice Kate Список пользователей после удаления компании Bob Kate

Удаление главной сущности — компании привело к удалению двух зависимых сущностей — пользователей.

Теперь изменим модели, указав необязательность наличия объекта Company:

public class Company < public int Id < get; set; >public string? Name < get; set; >public List Users < get; set; >= new(); > public class User < public int Id < get; set; >public string? Name < get; set; >public int? CompanyId < get; set; >public Company? Company < get; set; >>

Теперь внешний ключ имеет тип Nullable, то есть он допускает значение null. Когда пользователь не будет принадлежать ни одной компании, это свойство будет иметь значение null. И в этом случае скрипт таблицы Users будет выглядеть следующим образом:

CREATE TABLE "Users" ( "Id" INTEGER NOT NULL, "Name" TEXT, "CompanyId" INTEGER, CONSTRAINT "FK_Users_Companies_CompanyId" FOREIGN KEY("CompanyId") REFERENCES "Companies"("Id"), CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT) );

Аналогичная связь будет устанавливаться, если свойство-внешний ключа отсутствует, а навигационное свойство представляет nullable-тип:

public class User < public int Id < get; set; >public string? Name < get; set; >public Company? Company < get; set; >>

Если мы запустим ту же самую программу, то получим уже другой консольный вывод:

Bob Tom Alice Kate Список пользователей после удаления компании Bob Tom Alice Kate

Настройка каскадного удаления с помощью Fluent API

В Fluent API доступны три разных сценария, которые управляют поведением зависимой сущности в случае удаления главной сущности:

  • Cascade : зависимая сущность удаляется вместе с главной
  • SetNull : свойство-внешний ключ в зависимой сущности получает значение null
  • Restrict : зависимая сущность никак не изменяется при удалении главной сущности

Например, установим каскадное удаление, даже если по умолчанию оно не предусматривается:

using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public DbSet Companies < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Entity() .HasOne(u => u.Company) .WithMany(c => c.Users) .OnDelete(DeleteBehavior.Cascade); > > public class Company < public int Id < get; set; >public string? Name < get; set; >public List Users < get; set; >= new(); > public class User < public int Id < get; set; >public string? Name < get; set; >public Company? Company < get; set; >>

Соответственно чтобы отключить каскадное удаление, нам надо использовать вызов OnDelete(DeleteBehavior.SetNull) .

Что такое каскадное удаление

Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7

Последнее обновление: 16.11.2020

Каскадное удаление представляет автоматическое удаление зависимой сущности после удаления главной.

По умолчанию для сущностей применяется каскадное удаление, если наличие связанной сущности обязательно. Например:

public class Company < public int Id < get; set; >public string Name < get; set; >// название компании public List Users < get; set; >> public class User < public int Id < get; set; >public string Name < get; set; >public int CompanyId < get; set; >// внешний ключ public Company Company < get; set; >// навигационное свойство > public class ApplicationContext : DbContext < public DbSetCompanies < get; set; >public DbSet Users < get; set; >public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); >>

Здесь свойство внешнего ключа имеет тип int , оно не допускает значения null и требует наличия конкретного значения — id связанного объекта Company. То есть для объекта User обязательно необходимо наличия связанного объекта Company. Поэтому сгенерированная таблица Users будет иметь код:

CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, [CompanyId] INT NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Users_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies] ([Id]) ON DELETE CASCADE );

В определении внешнего ключа устанавливается каскадное удаление: ON DELETE CASCADE

Например, добавим в базу данных 2 компании и 4 связанных с ними пользователей и затем удалим одну из компаний:

using (ApplicationContext db = new ApplicationContext()) < // добавляем начальные данные Company microsoft = new Company < Name = "Microsoft" >; Company google = new Company < Name = "Google" >; db.Companies.AddRange(microsoft, google); db.SaveChanges(); User tom = new User < Name = "Tom", Company = microsoft >; User bob = new User < Name = "Bob", Company = google >; User alice = new User < Name = "Alice", Company = microsoft >; User kate = new User < Name = "Kate", Company = google >; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); // получаем пользователей var users = db.Users.ToList(); foreach (var user in users) Console.WriteLine($""); // Удаляем первую компанию var comp = db.Companies.FirstOrDefault(); db.Companies.Remove(comp); db.SaveChanges(); Console.WriteLine("\nСписок пользователей после удаления компании"); // снова получаем пользователей users = db.Users.ToList(); foreach (var user in users) Console.WriteLine($""); >

Консольный вывод программы:

Bob Tom Alice Kate Список пользователей после удаления компании Bob Kate

Удаление главной сущности — компании привело к удалению двух зависимых сущностей — пользователей.

Теперь изменим модели, указав необязательность наличия объекта Company:

public class Company < public int Id < get; set; >public string Name < get; set; >// название компании public List Users < get; set; >> public class User < public int Id < get; set; >public string Name < get; set; >public int? CompanyId < get; set; >// внешний ключ public Company Company < get; set; >// навигационное свойство >

Теперь внешний ключ имеет тип Nullable, то есть он допускает значение null. Когда пользователь не будет принадлежать ни одной компании, это свойство будет иметь значение null. И в этом случае скрипт таблицы Users будет выглядеть следующим образом:

CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, [CompanyId] INT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Users_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies] ([Id]) );

Если мы запустим ту же самую программу, то получим уже другой консольный вывод:

Bob Tom Alice Kate Список пользователей после удаления компании Bob Tom Alice Kate

Настройка каскадного удаления с помощью Fluent API

В Fluent API доступны три разных сценария, которые управляют поведением зависимой сущности в случае удаления главной сущности:

  • Cascade : зависимая сущность удаляется вместе с главной
  • SetNull : свойство-внешний ключ в зависимой сущности получает значение null
  • Restrict : зависимая сущность никак не изменяется при удалении главной сущности

Например, установим каскадное удаление, даже если по умолчанию оно не предусматривается:

using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using System.Collections.Generic; public class ApplicationContext : DbContext < public DbSetCompanies < get; set; >public DbSet Users < get; set; >public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Entity() .HasOne(p => p.Company) .WithMany(t => t.Users) .OnDelete(DeleteBehavior.Cascade); > > public class Company < public int Id < get; set; >public string Name < get; set; >public List Users < get; set; >> public class User < public int Id < get; set; >public string Name < get; set; >public int? CompanyId < get; set; >public Company Company < get; set; >>

Соответственно чтобы отключить каскадное удаление, нам надо использовать вызов OnDelete(DeleteBehavior.SetNull) .

Каскадное удаление объектов

Если в базе данных есть объекты типа Клиент , ссылающиеся на Адрес , то без дополнительных настроек при попытке удаления объекта типа Адрес произойдёт ошибка. База данных не даст удалить такой объект.

Варианты решения проблемы

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

Специальные интерфейсы

Для реализации каскадного удаления можно воспользоваться специально разработанными интерфейсами IReferencesCascadeDelete и IReferencesNullDelete.

Рекурсивное удаление

Это самый простой вариант, но и самый недружелюбный к пользователю: удаление 1 объекта может привести к удалению важной информации информации, связанной с данным объектом.

  • В бизнес-сервере мастера (в примере — Адрес ) вычитать все объекты, ссылающиеся на удаляемый.
  • Проставить всем объектам статус ObjectStatus.Deleted.
  • Отправить на удаление все объекты.
  • Повторить рекурсивно для всех объектов.

Фиктивный объект

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

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

Вариантов решения проблемы несколько:

  • создавать фиктивный объект при каждом удалении
  • создать по 1 фиктивному объекту для каждого класса и “вешать” все ссылки на него.

Алгоритм для второго варианта:

  • (один раз) Создать объект и записать его в базу. Запоминить его PrimaryKey, например, в файле конфигурации или в файле с константами.
  • В бизнес-сервере мастера (в примере — Адрес ) вычитать все объекты, ссылающиеся на удаляемый.
  • Проставить всем объектам ссылку на фиктивный объект.
  • Отправить на обновление все объекты.

Фиктивное удаление

При фиктивном удалении данные на самом деле не удаляются из базы, а всего лишь помечаются как удаленные. Во все объекты добавляется какое-нибудь поле типа bool . При удалении объекта в бизнес-сервере перехватывается объект, у него меняется статус с Deleted на Altered и изменяется поле Актуально = false; .

После этого объект уходит на обновление в базу и остается в ней, но считается удаленным. Разумеется, необходимо реализовывать логику, которая будет “считать” такие объекты удаленными: при выводе информации пользователю необходимо накладывать ограничения на выводимые данные.

Note: Такой способ позволяет восстанавливать удаленные объекты.

Пример

Необходимо доработать диаграмму классов таким образом, чтобы она поддерживала фиктивное удаление: добавить поле Актуально:bool .

Добавить логику в бизнес-сервера объектов (на примере Адреса ):

if (UpdatedObject.GetStatus() == ObjectStatus.Deleted)  // Не дадим объекту удалиться, но выставим флаг Актуальности. UpdatedObject.SetStatus(ObjectStatus.Altered); UpdatedObject.Актуально = false; // Найдем все объекты, ссылающиеся на "удаляемый" и удалим их. var ds = (SQLDataService)DataServiceProvider.DataService; var klients = ds.QueryКлиент>(Клиент.Views.КлиентE) .Where(k => k.Прописка.__PrimaryKey == UpdatedObject.__PrimaryKey); foreach (var k in klients)  k.SetStatus(ObjectStatus.Deleted); > return klients.ToArray(); > 

Note: Внимание! Cсылающиеся объекты отправленные на удаление, но они точно также перехватятся в своем бизнес-сервере и не удалятся.

Далее, чтобы пользователю не выводились “удаленные” данные при просмотре списка объектов, требуется на соответствующий контрол наложить ограничение вида:

var ds = (MSSQLDataService)DataServiceProvider.DataService; IQueryableКлиент> limit1 = ds.QueryАдрес>(Адрес.Views.АдресL).Where(Address => Address.Актуально); Function onlyActual = LinqToLcs.GetLcs(limit1.Expression, Адрес.Views.АдресL).LimitFunction; 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *