Расширенное сопоставление таблиц

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

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

Разбиение таблиц

EF Core позволяет сопоставить две или несколько сущностей с одной строкой. Это называется разделением таблиц или общим доступом к таблицам.

Настройка

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

Распространенный сценарий разделения таблиц использует только подмножество столбцов в таблице для повышения производительности или инкапсуляции.

В этом примере Order представляет подмножество DetailedOrder.

public class Order
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public string BillingAddress { get; set; }
    public string ShippingAddress { get; set; }
    public byte[] Version { get; set; }
}

Помимо требуемой конфигурации, которую мы вызываем Property(o => o.Status).HasColumnName("Status") для сопоставления DetailedOrder.Status с тем же столбцом, что Order.Statusи .

modelBuilder.Entity<DetailedOrder>(
    dob =>
    {
        dob.ToTable("Orders");
        dob.Property(o => o.Status).HasColumnName("Status");
    });

modelBuilder.Entity<Order>(
    ob =>
    {
        ob.ToTable("Orders");
        ob.Property(o => o.Status).HasColumnName("Status");
        ob.HasOne(o => o.DetailedOrder).WithOne()
            .HasForeignKey<DetailedOrder>(o => o.Id);
        ob.Navigation(o => o.DetailedOrder).IsRequired();
    });

Совет

Дополнительные сведения см. в полном примере проекта .

Использование

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

using (var context = new TableSplittingContext())
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.Add(
        new Order
        {
            Status = OrderStatus.Pending,
            DetailedOrder = new DetailedOrder
            {
                Status = OrderStatus.Pending,
                ShippingAddress = "221 B Baker St, London",
                BillingAddress = "11 Wall Street, New York"
            }
        });

    context.SaveChanges();
}

using (var context = new TableSplittingContext())
{
    var pendingCount = context.Orders.Count(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"Current number of pending orders: {pendingCount}");
}

using (var context = new TableSplittingContext())
{
    var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}

Необязательная зависимые сущности

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

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

Маркеры параллелизма

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

Чтобы избежать предоставления маркера параллелизма в потребляемом коде, можно создать его как теневое свойство:

modelBuilder.Entity<Order>()
    .Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

modelBuilder.Entity<DetailedOrder>()
    .Property(o => o.Version).IsRowVersion().HasColumnName("Version");

Наследование

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

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

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

Разбиение сущностей

EF Core позволяет сопоставить сущность с строками в двух или более таблицах. Это называется разделением сущностей.

Настройка

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

  • Таблица для сведений о клиенте Customers
  • PhoneNumbers Таблица для номера телефона клиента
  • Addresses Таблица для адреса клиента

Ниже приведены определения для этих таблиц в SQL Server:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

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

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Это достигается в EF7 путем вызова SplitToTable каждого разделения в типе сущности. Например, следующий код разбивает Customer тип сущности на CustomersPhoneNumbersAddresses и таблицы, показанные выше:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

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

Настройка связывания внешнего ключа

FK, связывающие сопоставленные таблицы, нацелены на те же свойства, на которые она объявлена. Обычно он не будет создан в базе данных, так как он будет избыточным. Но существует исключение, если тип сущности сопоставляется с несколькими таблицами. Чтобы изменить аспекты, можно использовать API Fluent для конфигурации отношений:

modelBuilder.Entity<Customer>()
    .HasOne<Customer>()
    .WithOne()
    .HasForeignKey<Customer>(a => a.Id)
    .OnDelete(DeleteBehavior.Restrict);

Ограничения

  • Разделение сущностей нельзя использовать для типов сущностей в иерархиях.
  • Для любой строки в главной таблице должна быть строка в каждой из разделенных таблиц (фрагменты не являются необязательными).

Конфигурация аспектов для конкретной таблицы

Некоторые шаблоны сопоставления приводят к тому, что одно и то же свойство CLR сопоставляется с столбцом в каждой из нескольких разных таблиц. EF7 позволяет этим столбцам иметь разные имена. Например, рассмотрим простую иерархию наследования:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

С помощью стратегии сопоставления наследования TPT эти типы будут сопоставлены с тремя таблицами. Однако столбец первичного ключа в каждой таблице может иметь другое имя. Например:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 позволяет настроить это сопоставление с помощью вложенного построителя таблиц:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

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

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 поддерживает это сопоставление таблиц:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });