Сопоставление EF Core TPC сущности и производной сущности

У меня есть объекты EF Core, которые определены как TPC.

Объекты:

public class Customer
{
  public long Id { get; set; }
  public ICollection<ProformaInvoice> ProformaInvoices { get; set; }
  public ICollection<Invoice> Invoices { get; set; }
}

public class Invoice
{
  public long Id { get; set; }
  public double Value { get; set; }
  public Customer Customer { get; set; }
}

public class ProformaInvoice : Invoice       // configured as TPC
{
  public string Notes { get; set; }
}

Контекст:

public class MyContext : DbContext
{

  protected override void OnModelCreating(ModelBuilder builder)
  {
    base.OnModelCreating(builder);

    var customerBuilder = builder.Entity<Customer>();
    customerBuilder.HasKey(x => x.Id);
    customerBuilder.HasMany(x => x.ProformaInvoices).WithOne(x => x.Customer).OnDelete(DeleteBehavior.Cascade);
    customerBuilder.HasMany(x => x.Invoices).WithOne(x => x.Customer).OnDelete(DeleteBehavior.Cascade);

    var invoiceBuilder = builder.Entity<Invoice>();
    invoiceBuilder.UseTpcMappingStrategy();             // TPC
    invoiceBuilder.HasKey(x => x.Id);
    invoiceBuilder.Property(x => x.Value).IsRequired();

    var proformaInvoiceBuilder = builder.Entity<ProformaInvoice>();
    //proformaInvoiceBuilder.HasKey(x => x.Id);         // do not configure key else get error "A key cannot be configured on 'ProformaInvoice' because it is a derived type. The key must be configured on the root type 'Invoice'."
    proformaInvoiceBuilder.Property(x => x.Value).IsRequired();
    proformaInvoiceBuilder.Property(x => x.Notes).IsRequired();
  }

  public DbSet<Customer> Customers => Set<Customer>();
  public DbSet<ProformaInvoice> ProformaInvoices => Set<ProformaInvoice>();
  public DbSet<Invoice> Invoices => Set<Invoice>();

}

Создать миграцию:

$ dotnet ef migrations add AddEntities

Ошибка:

Невозможно создать связь между «Клиент.Счета-фактуры» и «Счет-фактура.Клиент», поскольку связь между «Клиент.Счета-проформы» и «Счет-проформа.Клиент» уже существует. Навигации могут участвовать только в одном отношении. Если вы хотите переопределить существующие отношения, сначала вызовите «Игнорировать» в навигации «Счет-фактура. Клиент» в «OnModelCreating».

Как это исправить? Я подозреваю, что это как-то связано с конструкцией TPC.

(Обратите внимание, что я не могу перейти на TPH/TPT, мне нужно использовать TPC... это просто упрощенная минимальная реплика.)

В ожидании стандартной критики SO... К сожалению, я не могу «изменить дизайн», поскольку он представляет собой устаревшую систему, находящуюся вне моего контроля. :-)

lonix 01.04.2023 15:23

Вы понимаете, что Customer.Invoices включает в себя ProformaInvoices?

Gert Arnold 01.04.2023 16:08

@GertArnold Я подозревал это, но не был уверен - спасибо за разъяснение. Это не то, чего я хочу. Как их "разделить"? Пока не так много информации о TPC... Судя по тому, что я видел в выступлениях сообщества EF, использование TPC, как я это делал, — для сопоставления 1: 1 из пространства объектов в пространство базы данных при сохранении объектов DRY — это поддерживаемая функция. Но я не уверен, как это сделать?

lonix 01.04.2023 16:45

Не могли бы вы показать существующую/желаемую схему базы данных?

Guru Stron 01.04.2023 18:05

Какой дизайн вы не можете изменить — базу данных, модель объекта, и то, и другое? Сомневаюсь, что кто-то может требовать TPC. Как я понимаю, у вас есть две отдельные таблицы, имеющие "общий" набор похожих полей. Для этого вам не нужно наследование EF. Просто используйте их как отдельные объекты, тот факт, что вы хотите, чтобы они были связаны с отдельными коллекциями, противоречит правилам объектно-ориентированного программирования, согласно которым Collection<Base> включает в себя как Base, так и Derived. Стратегии наследования EFC не нарушают это правило, просто изменяют способ хранения иерархии в базе данных.

Ivan Stoev 01.04.2023 19:07

@IvanStoev Спасибо за объяснение. В стендапе сообщества (сейчас не могу найти точную временную метку) сказали, что использование TPC только для моделирования отдельных сущностей, как это сделал я, поддерживается... Но то, что я показал выше, не работает по причинам ты объяснил. Как мне смоделировать два класса, как указано выше, которые имеют совершенно отдельные таблицы? (Это означает не TPH или TPT, поэтому я попробовал новый TPC.) Вероятно, это настолько просто, что я слишком много думаю об этом.

lonix 02.04.2023 09:00

@lonix См. Ниже. Постарался охватить все возможные на данный момент варианты отображения, надеюсь поможет. Ваше здоровье.

Ivan Stoev 02.04.2023 13:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
149
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Наследование представляет собой отношение «является», что означает, что производное может использоваться где угодно в качестве замены основания. Стратегии наследования EF Core просто изменяют способ хранения/извлечения иерархии объектов в/из базы данных, но не нарушают это правило, поэтому и DbSet<Base>, и ICollection<Base> также будут включать (и разрешать добавление) производные элементы.

Это означает, что если вы хотите различать два набора/коллекции типов сущностей, их типы сущностей элементов (обратите внимание, типы сущностей, а не типы CLR) не должны иметь отношения наследования.

В EF Core доступны следующие параметры сопоставления:

Во-первых, не использовать какую-либо стратегию наследования EFC, что достигается с помощью HasBaseType Fluent API и передачи null типа (другими словами, говоря: «Этот тип сущности не имеет базовой сущности, даже если класс наследуется от нее), например.

var invoiceBuilder = modelBuilder.Entity<Invoice>();
invoiceBuilder.HasKey(x => x.Id);
invoiceBuilder.Property(x => x.Value).IsRequired();

var proformaInvoiceBuilder = modelBuilder.Entity<ProformaInvoice>();
proformaInvoiceBuilder.HasBaseType((Type)null);
proformaInvoiceBuilder.HasKey(x => x.Id);
proformaInvoiceBuilder.Property(x => x.Value).IsRequired();
proformaInvoiceBuilder.Property(x => x.Notes).IsRequired();

var customerBuilder = modelBuilder.Entity<Customer>();
customerBuilder.HasKey(x => x.Id);
customerBuilder.HasMany(x => x.Invoices).WithOne(x => x.Customer).OnDelete(DeleteBehavior.Cascade);
customerBuilder.HasMany(x => x.ProformaInvoices).WithOne(x => x.Customer).OnDelete(DeleteBehavior.Cascade);

Однако обратите внимание, что эта строка

customerBuilder.HasMany(x => x.ProformaInvoices).WithOne(x => x.Customer).OnDelete(DeleteBehavior.Cascade);

должно быть после

proformaInvoiceBuilder.HasBaseType((Type)null);

в противном случае вы получите такое же исключение «используемое свойство навигации». Что может быть проблематично, если вы используете классы конфигурации типов сущностей и не контролируете порядок выполнения. Следовательно, лучше настроить эти отношения с другой стороны (после настройки задействованного объекта), например. (теперь они могут быть в любом порядке)


var customerBuilder = modelBuilder.Entity<Customer>();
customerBuilder.HasKey(x => x.Id);

var invoiceBuilder = modelBuilder.Entity<Invoice>();
invoiceBuilder.HasKey(x => x.Id);
invoiceBuilder.Property(x => x.Value).IsRequired();
invoiceBuilder.HasOne(x => x.Customer).WithMany(x => x.Invoices).OnDelete(DeleteBehavior.Cascade);

var proformaInvoiceBuilder = modelBuilder.Entity<ProformaInvoice>();
proformaInvoiceBuilder.HasBaseType((Type)null);
proformaInvoiceBuilder.HasKey(x => x.Id);
proformaInvoiceBuilder.Property(x => x.Value).IsRequired();
proformaInvoiceBuilder.Property(x => x.Notes).IsRequired();
proformaInvoiceBuilder.HasOne(x => x.Customer).WithMany(x => x.ProformaInvoices).OnDelete(DeleteBehavior.Cascade);

Второй вариант - переместить общие данные в отдельный абстрактный класс и позволить им наследоваться от него, например.

public abstract class BaseInvoice
{
    public long Id { get; set; }
    public double Value { get; set; }
    public Customer Customer { get; set; }
}

public class Invoice : BaseInvoice
{
}

public class ProformaInvoice : BaseInvoice
{
    public string Notes { get; set; }
}

и используйте любую из приведенных выше быстрых конфигураций только с удаленным HasBaseType((Type)null), так как теперь база двух сущностей — это просто класс, не отображаемый как сущность.

Или теперь вы можете сопоставить этот класс как сущность и стратегию TPC.


var customerBuilder = modelBuilder.Entity<Customer>();
customerBuilder.HasKey(x => x.Id);

var baseInvoiceBuilder = modelBuilder.Entity<BaseInvoice>();
baseInvoiceBuilder.UseTpcMappingStrategy();
baseInvoiceBuilder.HasKey(x => x.Id);
baseInvoiceBuilder.Property(x => x.Value).IsRequired();

var invoiceBuilder = modelBuilder.Entity<Invoice>();
invoiceBuilder.HasOne(x => x.Customer).WithMany(x => x.Invoices).OnDelete(DeleteBehavior.Cascade);

var proformaInvoiceBuilder = modelBuilder.Entity<ProformaInvoice>();
proformaInvoiceBuilder.Property(x => x.Notes).IsRequired();
proformaInvoiceBuilder.HasOne(x => x.Customer).WithMany(x => x.ProformaInvoices).OnDelete(DeleteBehavior.Cascade);

Просто имейте в виду, что базовая сущность вместе со стратегией должны быть настроены до производных сущностей. Так же как и базовые свойства, ключ и т.д. должны быть настроены там, а не в производных сущностях. Это, наряду с тем фактом, что TPC ожидает, что ключи будут уникальными для всех таблиц, поэтому обычные столбцы «идентификации» не могут использоваться, и вместо этого требуется общая последовательность, все это только для того, чтобы предоставить вам запрос на объединение, который вам, вероятно, не нужен. , делает TPC бесполезным для меня. Но в любом случае, выше указано, как вы можете его использовать, если настаиваете.

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

dbContext.Set<Invoice>().ToQueryString()

dbContext.Set<ProformaInvoice>().ToQueryString()

должен запрашивать только соответствующую таблицу

SELECT [i].[Id], [i].[Value], [i].[CustomerId]
FROM [Invoices] AS [i]

SELECT [p].[Id], [p].[Value], [p].[CustomerId], [p].[Notes]
FROM [ProformaInvoices] AS [p]

и оба

dbContext.Set<Customer>().SelectMany(c => c.Invoices).ToQueryString()

dbContext.Set<Customer>().SelectMany(c => c.ProformaInvoices).ToQueryString()

должен присоединяться только к соответствующей таблице

SELECT [i].[Id], [i].[Value], [i].[CustomerId]
FROM [Customers] AS [c]
INNER JOIN [Invoices] AS [i] ON [c].[Id] = [i].[CustomerId]

SELECT [p].[Id], [p].[Value], [p].[CustomerId], [p].[Notes]
FROM [Customers] AS [c]
INNER JOIN [ProformaInvoices] AS [p] ON [c].[Id] = [p].[CustomerId]

и только и только если вы использовали самое последнее сопоставление BaseInvoice в качестве базового объекта TPC, следующие

dbContext.Set<BaseInvoice>().ToQueryString()

должен образовать союз двух

SELECT [i].[Id], [i].[Value], [i].[CustomerId], NULL AS [CustomerId0], NULL AS [Notes], N'Invoice' AS [Discriminator]
FROM [Invoices] AS [i]
UNION ALL
SELECT [p].[Id], [p].[Value], NULL AS [CustomerId], [p].[CustomerId] AS [CustomerId0], [p].[Notes], N'ProformaInvoice' AS [Discriminator]
FROM [ProformaInvoices] AS [p]

Спасибо Иван за исчерпывающий ответ! HasBaseType это то, что я искал. Документы кратко освещают это, но не так хорошо, как в вашем ответе.

lonix 02.04.2023 14:21

Другие вопросы по теме