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

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

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

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

using Microsoft.EntityFrameworkCore;


public class Company
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public List<User> 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 DbSet<User> Users { get; set; } = null!;
    public DbSet<Company> 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<User> 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<int>, то есть он допускает значение 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 DbSet<User> Users { get; set; } = null!;
    public DbSet<Company> 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<User>()
            .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<User> 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).

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850