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

у меня возникли проблемы с созданием сопоставления кода Entity Framework для следующей схемы базы данных (в 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; }
}

отображение с беглым 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 });
    

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

  • отказаться от наследования отображения и создать один к одному сопоставлений между 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?

3 ответов


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

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

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


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

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

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


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

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


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

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