Наследование и составные внешние ключи - одна часть ключа в базовом классе, другая часть производного класса

У меня возникают проблемы с созданием преобразования Entity Framework Code-First для следующей примерной схемы базы данных (в SQL Server):

Database schema with composite foreign keys

Каждая таблица содержит TenantId, которая является частью всех (составных) первичных и внешних ключей (Multi-Tenancy).

A Company является либо Customer, либо Supplier, и я пытаюсь моделировать это посредством отображения наследования типа Table-Per-Type (TPT):

public abstract class Company
{
    public int TenantId { get; set; }
    public int CompanyId { get; set; }

    public int AddressId { get; set; }
    public Address Address { get; set; }
}

public class Customer : Company
{
    public string CustomerName { get; set; }

    public int SalesPersonId { get; set; }
    public Person SalesPerson { get; set; }
}

public class Supplier : Company
{
    public string SupplierName { get; set; }
}

Сопоставление с Fluent API:

modelBuilder.Entity<Company>()
    .HasKey(c => new { c.TenantId, c.CompanyId });

modelBuilder.Entity<Customer>()
    .ToTable("Customers");

modelBuilder.Entity<Supplier>()
    .ToTable("Suppliers");

Базовая таблица Companies имеет отношение "один ко многим" к Address (каждая компания имеет адрес, независимо от того, клиент или поставщик), и я могу создать сопоставление для этой ассоциации:

 modelBuilder.Entity<Company>()
     .HasRequired(c => c.Address)
     .WithMany()
     .HasForeignKey(c => new { c.TenantId, c.AddressId });

Внешний ключ состоит из одной части первичного ключа - TenantId - и отдельной колонки - AddressId. Это работает.

Как видно из схемы базы данных, из точки базы данных соотношение между Customer и Person в основном является одним и тем же отношением "один ко многим" между Company и Address - внешним ключом снова состоит из TenantId (часть первичного ключа) и столбца SalesPersonId. (Только у клиента есть продавец, а не Supplier, поэтому отношение находится в производном классе на этот раз, а не в базовом классе.)

Я пытаюсь создать сопоставление для этой связи с Fluent API так же, как и раньше:

modelBuilder.Entity<Customer>()
    .HasRequired(c => c.SalesPerson)
    .WithMany()
    .HasForeignKey(c => new { c.TenantId, c.SalesPersonId });

Но когда EF пытается скомпилировать модель, бросается InvalidOperationException:

Внешний ключевой компонент "TenantId" не является объявленным свойством на введите "Клиент". Убедитесь, что он не был явно исключен из модель и что это действительное примитивное свойство.

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

Я попробовал две модификации, чтобы заставить его работать, возможно:

  • Изменена связь внешнего ключа между Customer и Person с независимой ассоциацией, т.е. удалила свойство SalesPersonId, а затем попыталась отобразить:

    modelBuilder.Entity<Customer>()
        .HasRequired(c => c.SalesPerson)
        .WithMany()
        .Map(m => m.MapKey("TenantId", "SalesPersonId"));
    

    Это не помогает (я действительно не надеялся, это было бы), и исключение:

    Указанная схема недействительна.... Каждое имя свойства в типе должно быть уникальный. Имя свойства "TenantId" уже определено.

  • Изменено отображение TPT для TPH, т.е. удалены два вызова ToTable. Но он вызывает одно и то же исключение.

Я вижу два обходных пути:

  • Ввести SalesPersonTenantId в класс Customer:

    public class Customer : Company
    {
        public string CustomerName { get; set; }
    
        public int SalesPersonTenantId { get; set; }
        public int SalesPersonId { get; set; }
        public Person SalesPerson { get; set; }
    }
    

    и отображение:

    modelBuilder.Entity<Customer>()
        .HasRequired(c => c.SalesPerson)
        .WithMany()
        .HasForeignKey(c => new { c.SalesPersonTenantId, c.SalesPersonId });
    

    Я тестировал это, и он работает. Но в дополнение к TenantId у меня будет столбец SalesPersonTenantId в таблице Customers. Этот столбец является избыточным, поскольку оба столбца всегда должны иметь одинаковое значение с точки зрения бизнеса.

  • Отображение наследования наложения и создание взаимно-однозначных сопоставлений между Company и Customer и между Company и Supplier. Company должен стать конкретным типом, а не абстрактным, и я бы имел два навигационных свойства в Company. Но эта модель не будет правильно указывать, что компания является либо клиентом, либо поставщиком и не может быть одновременно одновременно. Я не тестировал его, но я считаю, что это сработает.

Я вставляю полный пример, который я тестировал (консольное приложение, ссылка на сборку EF 4.3.1, загружаемое через NuGet) здесь, если кому-то нравится экспериментировать с ним:

using System;
using System.Data.Entity;

namespace EFTPTCompositeKeys
{
    public abstract class Company
    {
        public int TenantId { get; set; }
        public int CompanyId { get; set; }

        public int AddressId { get; set; }
        public Address Address { get; set; }
    }

    public class Customer : Company
    {
        public string CustomerName { get; set; }

        public int SalesPersonId { get; set; }
        public Person SalesPerson { get; set; }
    }

    public class Supplier : Company
    {
        public string SupplierName { get; set; }
    }

    public class Address
    {
        public int TenantId { get; set; }
        public int AddressId { get; set; }

        public string City { get; set; }
    }

    public class Person
    {
        public int TenantId { get; set; }
        public int PersonId { get; set; }

        public string Name { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Company> Companies { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Person> Persons { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Company>()
                .HasKey(c => new { c.TenantId, c.CompanyId });

            modelBuilder.Entity<Company>()
                .HasRequired(c => c.Address)
                .WithMany()
                .HasForeignKey(c => new { c.TenantId, c.AddressId });

            modelBuilder.Entity<Customer>()
                .ToTable("Customers");

            // the following mapping doesn't work and causes an exception
            modelBuilder.Entity<Customer>()
                .HasRequired(c => c.SalesPerson)
                .WithMany()
                .HasForeignKey(c => new { c.TenantId, c.SalesPersonId });

            modelBuilder.Entity<Supplier>()
                .ToTable("Suppliers");

            modelBuilder.Entity<Address>()
                .HasKey(a => new { a.TenantId, a.AddressId });

            modelBuilder.Entity<Person>()
                .HasKey(p => new { p.TenantId, p.PersonId });
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
            using (var ctx = new MyContext())
            {
                try
                {
                    ctx.Database.Initialize(true);
                }
                catch (Exception e)
                {
                    throw;
                }
            }
        }
    }
}

Вопрос: Есть ли способ сопоставить схему базы данных выше с моделью класса с Entity Framework?

Ответы

Ответ 1

Ну, я не могу ничего комментировать, поэтому я добавляю это как ответ.

Я создал проблему для CodePlex для этой проблемы, поэтому, надеюсь, они скоро ее рассмотрят. Оставайтесь с нами!

http://entityframework.codeplex.com/workitem/865


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

Цитата из команды Entity Framework в CodePlex:

Это часть более фундаментального ограничения, когда EF не поддерживает имеющий свойство, определенное в базовом типе, а затем используя его как внешний ключ в производном типе. К сожалению, это ограничение, которое было бы очень сложно удалить из нашей базы кода. Учитывая, что у нас нет видел много запросов на это, это не то, что мы планируем адрес на этом этапе, поэтому мы закрываем эту проблему.

Ответ 2

Не решение, но обходное решение (*): хорошим выбором является использование отдельных столбцов идентификатора (как), обычно с автоматическим добавлением и обеспечение целостности базы данных с использованием внешних ключей, уникальных индексов и т.д. Более сложная целостность данных может быть достигнуто с помощью триггеров, так что, возможно, вы могли бы возглавить этот путь, но вы можете оставить это на уровне бизнес-логики приложения, если приложение действительно не ориентировано на данные. Но поскольку вы используете Entity Framework, вероятно, можно предположить, что это не ваш случай...?

(*), как предложено ivowiblo

Ответ 3

Я думаю, что это проще и снижает сложность, чтобы иметь Table → TableId (PK) → другие столбцы, включая FK.

Итак, в вашем примере - добавление столбца CustomerId в таблицу Customers позволит решить вашу проблему.