Наследование и составные внешние ключи - одна часть ключа в базовом классе, другая часть производного класса
У меня возникают проблемы с созданием преобразования 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 позволит решить вашу проблему.