Herança e chaves externas compostas - uma parte da chave na classe base, a outra parte na classe derivada
Estou tendo problemas para criar um mapeamento Code-First do Entity Framework para o seguinte esquema de banco de dados de amostra (no SQL Server):
Cada mesa contém umTenantId
que faz parte de todas as chaves primárias e estrangeiras (compostas) (Multi-Tenancy).
A Company
é umCustomer
ou umSupplier
e eu tento modelar isso via mapeamento de herança Tabela-Por-Tipo (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; }
}
Mapeamento com a API do Fluent:
modelBuilder.Entity<Company>()
.HasKey(c => new { c.TenantId, c.CompanyId });
modelBuilder.Entity<Customer>()
.ToTable("Customers");
modelBuilder.Entity<Supplier>()
.ToTable("Suppliers");
A mesa baseCompanies
tem um relacionamento um-para-muitos para umAddress
(cada empresa tem um endereço, não importa se cliente ou fornecedor) e eu posso criar um mapeamento para esta associação:
modelBuilder.Entity<Company>()
.HasRequired(c => c.Address)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.AddressId });
A chave estrangeira é composta por uma parte da chave primária - aTenantId
- e uma coluna separada - oAddressId
. Isso funciona.
Como você pode ver no esquema do banco de dados, da perspectiva do banco de dados, a relação entreCustomer
ePerson
é basicamente o mesmo tipo de relação um-para-muitos entreCompany
eAddress
- a chave estrangeira é composta novamente doTenantId
(parte da chave primária) e a colunaSalesPersonId
. (Apenas um cliente tem um vendedor, não umSupplier
, portanto, o relacionamento está na classe derivada desta vez, não na classe base.)
Eu tento criar um mapeamento para essa relação com a API do Fluent da mesma maneira que antes:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.SalesPersonId });
Mas quando a EF tenta compilar o modeloInvalidOperationException
é lançado:
O componente de chave estrangeira 'TenantId' não é uma propriedade declarada no tipo 'Cliente'. Verifique se ele não foi explicitamente excluído do modelo e se é uma propriedade primitiva válida.
Aparentemente, não consigo compor uma chave estrangeira de uma propriedade na classe base e de outra propriedade na classe derivada (embora no esquema do banco de dados a chave estrangeira seja composta de colunasambos na tabela do tipo derivadoCustomer
).
Eu tentei duas modificações para fazê-lo funcionar, talvez:
Mudou a associação da chave estrangeira entreCustomer
ePerson
para uma associação independente, ou seja, removeu a propriedadeSalesPersonId
e, em seguida, tentei o mapeamento:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.Map(m => m.MapKey("TenantId", "SalesPersonId"));
Isso não ajuda (eu realmente não esperava, seria) e a exceção é:
Esquema especificado não é válido. ... Cada nome de propriedade em um tipo deve ser exclusivo. O nome da propriedade 'TenantId' já estava definido.
Mudou o TPT para o mapeamento TPH, ou seja, removeu os doisToTable
chamadas. Mas lança a mesma exceção.
Eu vejo duas soluções alternativas:
Introduzir umSalesPersonTenantId
noCustomer
classe:
public class Customer : Company
{
public string CustomerName { get; set; }
public int SalesPersonTenantId { get; set; }
public int SalesPersonId { get; set; }
public Person SalesPerson { get; set; }
}
e o mapeamento:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.SalesPersonTenantId, c.SalesPersonId });
Eu testei isso e funciona. Mas vou ter uma nova colunaSalesPersonTenantId
noCustomers
mesa para além doTenantId
. Essa coluna é redundante porque as duas colunas sempre devem ter o mesmo valor da perspectiva de negócios.
Abandone o mapeamento de herança e crie mapeamentos um-para-um entreCompany
eCustomer
e entreCompany
eSupplier
. Company
deve se tornar um tipo concreto, em seguida, não abstrato e eu teria duas propriedades de navegação emCompany
. Mas este modelo não expressaria corretamente que uma empresa éou um clienteou um fornecedor e não pode ser ambos ao mesmo tempo. Eu não testei, mas acredito que funcionaria.
Eu colo o exemplo completo com o qual testei (aplicativo de console, referência à montagem do EF 4.3.1, baixado via NuGet) aqui, se alguém gosta de experimentá-lo:
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;
}
}
}
}
}
Pergunta: Existe alguma maneira de mapear o esquema do banco de dados acima para um modelo de classe com o Entity Framework?