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

У меня проблемы с созданием сопоставления кода в 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?

 Ivo09 июн. 2012 г., 16:26
Зачем тебе это надо? Почему бы просто не иметь один столбец Id и избежать головной боли?
 Slauma13 дек. 2012 г., 22:39
@luri: Нет, у меня нет решения, в настоящее время я использую обходной путь, упомянутый в моем последнем пункте выше & quot;Abandon inheritance mapping and create one-to-one mappings...& Quot;
 Slauma09 июн. 2012 г., 16:45
@ivowiblo: потому что я хочу обеспечить в базе данных, чтобы отношения между строками в разных таблицах всегда ссылались на данные одного и того же владельца. Если бы у меня был один столбец Id, у меня мог бы быть клиент арендатора 1, ссылающийся на SalesPerson арендатора 2, который будет действителен в БД. Я должен был убедиться, что это не происходит на уровне приложений. Ошибка будет катастрофической (арендатор 1 может получить доступ к данным арендатора 2). Но, тем не менее, ваше замечание является хорошим моментом, и я должен рассмотреть его, если нет другого решения.
 iuristona14 дек. 2012 г., 14:32
@ Слаума: Спасибо. Я добавлю обходной путь избыточного подхода. Я думаю, что удерживать это ограничение на уровне базы данных - это недорого. И если когда-нибудь мы исправим ошибку, я просто смогу удалить избыточный атрибут.
 iuristona13 дек. 2012 г., 21:22
@Slauma: Привет, парень, ты нашел какое-нибудь решение или обходной путь? У меня та же проблема, и я не знаю, как действовать дальше. Я также пытался например. включить версию переопределения для int TenantId в класс Customer. Но все равно не работает.

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

TableId (PK) - & gt; другие столбцы, включая ФК.

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

 Slauma16 сент. 2012 г., 22:25
Нет дублирования, но есть возможность связать клиента и человека двух разных арендаторов, что не должно происходить в бизнес-модели. Мой вопрос касается случая, когда я не могу использовать этот "простой общий шаблон" Вы рекомендуете (и Луис Феррейра в его ответе также).
 Slauma16 сент. 2012 г., 17:50
AAAAARRRRGGGHHHHHH! Извините :( Но ваш ответ такой: я спрашиваю "Я хочу построить собственную домашнюю метеостанцию, которая может измерять температуру и атмосферное давление. Как я могу это сделать?", А затем вы отвечаете: "Измерение давления слишком много, mplex. Постройте домашнюю метеостанцию, которая может только измерять температуру. Это решит вашу проблему. & quot;
 18 сент. 2012 г., 14:35
Я согласен с вами, что люди должны быть связаны с базовым типом (компании), а не с производными типами. Может быть, попытаться создать модель сначала с помощью связывания таким образом и проверить созданный код?
 28 сент. 2012 г., 21:39
@Slauma: как насчет создания триггера, чтобы справиться с этой ситуацией? «возможность связать клиента и человека двух разных арендаторов»; Деловой мир приложений - это мир обходных путей, делать его слишком сложно. И помните - вы НЕ имеете дело со страховкой, так что могло бы быть и хуже.
 Slauma16 сент. 2012 г., 22:01
Как это решает проблему? Если вы имеете в виду один ключевой столбец вместо составного ключа, прочитайте два комментария под вопросом, где я описал причину, по которой я хочу составные ключи. Но вполне возможно, что я неправильно понял ваше предложение.
Решение Вопроса

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

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

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

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

This is part of a more fundamental limitation where EF doesn't support having a property defined in a base type and then using it as a foreign key in a derived type. Unfortunately this is a limitation that would be very hard to remove from our code base. Given that we haven't seen a lot of requests for it, it's not something we are planning to address at this stage so we are closing this issue.

 Slauma21 мар. 2013 г., 14:04
Я добавил результат из CodePlex в ответ.

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

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

 18 авг. 2012 г., 19:33
Это обходной путь, но, на мой взгляд, не очень хорошая практика. Это на самом деле будет более сложным и распространит вашу бизнес-логику (триггеры могут быть просто неприятны для разработки и отладки) Я хотел бы предположить, что стоит попытаться найти решение этой проблемы в архитектуре данных, прежде чем пытаться поставить под угрозу целостность реляционной базы данных, чтобы упростить программирование.

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