У меня возникли проблемы с созданием сопоставления Code-First в Entity Framework для следующего образца схемы базы данных (в SQL Server):
Every таблица содержит TenantId
, который является частью всех (составных) первичных и внешних ключей (Multi-Tenancy).
Компания
является либо Клиентом
, либо Поставщиком
, и я пытаюсь смоделировать это с помощью сопоставления наследования 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");
Базовая таблица Компании
имеет отношение «один ко многим» к Адресу
(у каждой компании есть адрес, независимо от того, клиент это или поставщик) и я могу создать сопоставление для этой ассоциации:
modelBuilder.Entity<Company>()
.HasRequired(c => c.Address)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.AddressId });
Внешний ключ состоит из одной части первичного ключа — TenantId
— и отдельного столбца — AddressId
. Это работает.
Как видно из схемы базы данных, с точки зрения базы данных отношения между Customer
и Person
в основном такие же отношения один ко многим, как между Company
и Address
— внешний ключ снова состоит из TenantId
(часть первичного ключа) и столбца SalesPersonId
.(Только у клиента есть продавец, а не Поставщик
, поэтому на этот раз отношение находится в производном классе, а не в базовом классе.)
Я пытаюсь создать отображение для этого отношения с помощью Fluent API такой же, как и раньше:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.SalesPersonId });
Но когда EF пытается скомпилировать модель, выдается InvalidOperationException
:
Компонент внешнего ключа «TenantId» не является объявленным свойством на введите «Клиент». Убедитесь, что он не был явно исключен из модели и что это допустимое примитивное свойство.
По-видимому, я не могу составить внешний ключ из свойства в базовом классе и из другого свойства в производном классе (хотя в схеме базы данных внешний ключ состоит из столбцов и в таблице производного типа Заказчик
).
Я попробовал две модификации, чтобы заставить его работать, возможно:
Изменил связь внешнего ключа между Customer
и Person
на независимую ассоциацию, т.е. удалил свойство SalesPersonId
, а затем попробовал сопоставление:
modelBuilder.Entity()
.HasRequired(c => c.SalesPerson)
.Со многими()
.Map(m => m.MapKey("TenantId", "SalesPersonId"));
Это не помогает (я на самом деле не надеялся, что поможет), и исключение:
Указанная схема недействительна. ... Каждое имя свойства в типе должно быть уникальный. Имя свойства TenantId уже определено.
Изменено сопоставление TPT с TPH, т. е. удалены два вызова ToTable
. Но выдает такое же исключение.
Я вижу два обходных пути:
Ввести SalesPersonTenantId
в класс Customer
:
открытый класс Customer : Company
{
публичная строка CustomerName { получить; задавать; }
публичный интервал SalesPersonTenantId { получить; задавать; }
общественный интервал SalesPersonId { получить; задавать; }
общественное лицо SalesPerson { получить; задавать; }
}
и сопоставление:
modelBuilder.Entity()
.HasRequired(c => c.SalesPerson)
.Со многими()
.HasForeignKey(c => new { c.SalesPersonTenantId, c.SalesPersonId});
Я проверил это, и это работает. Но у меня будет новый столбец SalesPersonTenantId
в таблице Customers
в дополнение к TenantId
. Этот столбец является избыточным, поскольку оба столбца всегда должны иметь одно и то же значение с точки зрения бизнеса.
Отменить сопоставление наследования и создать взаимно однозначное сопоставление между Компанией
и Заказчиком
и между Компанией
и Поставщиком
. 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?