Подход TPC - Table Per Class

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

Начиная с версии EF Core 7.0 во фреймворк была добавлена поддержка нового подхода к наследованию - TPC (Table Per Concrete Type / Таблица на каждый отдельный тип). Этот подход предполагает создание для каждой модели по отдельной таблицы. Столбцы в каждой таблице создаются по всем свойствам, в том числе и унаследованным. С одной стороны, может показаться, что это усложняет хранение данных. Но с другой стороны, TPC работает более оптимально по сравнению с TPT для многих типов запросов, так как количество таблиц, которые необходимо запрашивать, уменьшено. Кроме того, результаты из каждой таблицы объединяются с помощью sql-команды UNION ALL, что может быть значительно быстрее, чем объединение таблиц с помощью INNER JOIN, которое применяется в TPT.

Для применения подхода TPC для базовой сущности иерархии классов вызывается метод UseTpcMappingStrateg. Для этого переопределяется метод OnModelCreating() контекста данных:

using Microsoft.EntityFrameworkCore;

public class User
{
    public string Id { get; set; }=Guid.NewGuid().ToString();
    public string? Name { get; set; }
}
public class Employee : User
{
    public int Salary { get; set; }
}
public class Manager : User
{
    public string? Departament { get; set; }
}


public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!; 
    public DbSet<Employee> Employees { get; set; } = null!;
    public DbSet<Manager> Managers { 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>().UseTpcMappingStrategy();  // Используем стратегию TPC
    }
}

В случае с SQLite в бд будут создаваться следующие три таблицы:

CREATE TABLE "Users" (
    "Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
    "Name" TEXT NULL
);
CREATE TABLE "Employees" (
    "Id" TEXT NOT NULL CONSTRAINT "PK_Employees" PRIMARY KEY,
    "Name" TEXT NULL,
    "Salary" INTEGER NOT NULL
);
CREATE TABLE "Managers" (
    "Id" TEXT NOT NULL CONSTRAINT "PK_Managers" PRIMARY KEY,
    "Name" TEXT NULL,
    "Departament" TEXT NULL
);

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

Пример использования:

using (ApplicationContext db = new ApplicationContext())
{
    User user1 = new User { Name = "Tom" };
    User user2 = new User { Name = "Bob" };
    db.Users.Add(user1);
    db.Users.Add(user2);

    Employee employee = new Employee { Name = "Sam", Salary = 500 };
    db.Employees.Add(employee);

    Manager manager = new Manager { Name = "Robert", Departament = "IT" };
    db.Managers.Add(manager);

    db.SaveChanges();

    var users = db.Users.ToList();
    Console.WriteLine("Все пользователи");
    foreach (var user in users)
    {
        Console.WriteLine(user.Name);
    }

    Console.WriteLine("\nВсе работники");
    foreach (var emp in db.Employees.ToList())
    {
        Console.WriteLine(emp.Name);
    }
	
    Console.WriteLine("\nВсе менеджеры");
    foreach (var man in db.Managers.ToList())
    {
        Console.WriteLine(man.Name);
    }
}

Консольный вывод:

Все пользователи
Tom
Bob
Sam
Robert

Все работники
Sam

Все менеджеры
Robert

Генерация идентификаторов

Поход TPC имеет ограничения в плане использования свойств-идентфикаторов. Во-первых, важно понимать, что EF Core требует, чтобы все сущности в иерархии имели уникальное значение ключа, даже если сущности имеют разные типы. Таким образом, в примере выше у объекта Employee не может быть того же значения ключа Id, что и у объекта Manager. Во-вторых, в отличие от TPT, здесь нет общей таблицы, которая могла бы действовать как единственное место, где хранятся ключевые значения и могут быть сгенерированы. И здесь есть различные стратегии.

Явная установка Id

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

public class User
{
    public string Id { get; set; }=Guid.NewGuid().ToString();
....................

Здесь Id присваивается guid - уникальное значение, благодаря чему мы знаем, что у нас будет только один объект, который будут иметь ключ с определенным значением.

Это могут быть и числовые ключи, главное, что ключи добавляемых объектов не конфликтовали.

Неявная установка с помощью последовательности

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

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public DbSet<Employee> Employees { get; set; } = null!;
    public DbSet<Manager> Managers { get; set; } = null!;
    public ApplicationContext()
    {
        Database.EnsureDeleted();
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().UseTpcMappingStrategy();
    }
}

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

public class Employee : User
{
    public int Salary { get; set; }
}
public class Manager : User
{
    public string? Departament { get; set; }
}

На уровне базы данных MS SQL Server будут формироваться следующие таблицы:

CREATE TABLE [Users] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [UserSequence]),
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Users] PRIMARY KEY ([Id])
);
CREATE TABLE [Employees] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [UserSequence]),
    [Name] nvarchar(max) NULL,
    [Salary] int NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([Id])
);
CREATE TABLE [Managers] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [UserSequence]),
    [Name] nvarchar(max) NULL,
    [Departament] nvarchar(max) NULL,
    CONSTRAINT [PK_Managers] PRIMARY KEY ([Id])
);

Здесь мы видим, что все три таблицы имеют одно и то же определение столбца Id:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [UserSequence])

В данном случае UserSequence — это последовательность базы данных, созданная EF Core.

Остальные стратегии

Другие стратегии могут предусматривать установку на уровне бд генератора для идентификаторов. Для этого можно для настройки маппинга можно использовать метод UseIdentityColumn. Он принимает ряд параметров. В частности, первый параметр представляет начальное значение для id, а второй параметр - приращение для последующих добавляемых объектов. Например, определим следующий контекст данных:

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!; 
    public DbSet<Employee> Employees { get; set; } = null!;
    public DbSet<Manager> Managers { get; set; } = null!;
    public ApplicationContext()
    {
        Database.EnsureDeleted();
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().UseTpcMappingStrategy();
        modelBuilder.Entity<User>().ToTable("Users", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 1));
        modelBuilder.Entity<Employee>().ToTable("Employees", tb => tb.Property(e => e.Id).UseIdentityColumn(10000, 1));
        modelBuilder.Entity<Manager>().ToTable("Managers", tb => tb.Property(e => e.Id).UseIdentityColumn(20000, 1));
    }
}

Здесь id для объектов Employee будут начинаться с 10000, а для Manager - с 20000 и будут иметь приращение 1. Однако опять же надо учитывать последующие разрастание базы данных, чтобы при последующих добавлениях не было конфликта между Id в разных таблицах.

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

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