Наследование и составные внешние ключи - одна часть ключа в базовом классе, другая часть в производном классе
У меня проблемы с созданием сопоставления кода в Entity Framework для следующей схемы базы данных (в SQL Server):
Каждая таблица содержит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?