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

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

Database schema with composite foreign keys

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

Company это либоCustomer илиSupplier и я пытаюсь смоделировать это с помощью таблицы наследования таблиц на тип (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 брошен:

The foreign key component 'TenantId' is not a declared property on type 'Customer'. Verify that it has not been explicitly excluded from the model and that it is a valid primitive property.

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

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

Changed the foreign key association between Customer and Person to an independent association, i.e. removed the property SalesPersonId, and then tried the mapping:

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

It doesn't help (I didn't really hope, it would) and the exception is:

Schema specified is not valid. ... Each property name in a type must be unique. Property name 'TenantId' was already defined.

Changed TPT to TPH mapping, i.e. removed the two ToTable calls. But it throws the same exception.

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

Introduce a SalesPersonTenantId into the Customer class:

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

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

and the mapping:

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

I tested this and it works. But I will have a new column SalesPersonTenantId in the Customers table in addition to the TenantId. This column is redundant because both columns always must have the same value from business perspective.

Abandon inheritance mapping and create one-to-one mappings between Company and Customer and between Company and Supplier. Company must become a concrete type then, not abstract and I would have two navigation properties in Company. But this model wouldn't express correctly that a company is either a customer or a supplier and cannot be both at the same time. I didn't test it but I believe it would work.

Я вставляю полный пример, который я тестировал (консольное приложение, ссылка на сборку 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;
                }
            }
        }
    }
}

Question: Is there any way to map the database schema above to a class model with Entity Framework?

Ответы на вопрос(3)

Ваш ответ на вопрос