我在为以下示例数据库模式(在SQL Server中)创建实体框架代码优先映射时遇到问题:
每个表都包含 TenantId
,它是所有(复合)主键和外键(多租户)的一部分 .
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
中的列组成) .
我尝试了两次修改以使其正常工作:
- 将
Customer
和Person
之间的外键关联更改为独立关联,即删除了属性SalesPersonId
,然后尝试了映射:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.Map(m => m.MapKey("TenantId", "SalesPersonId"));
它没有帮助(我真的不希望,它会)并且例外是:
指定的架构无效 . ...类型中的每个属性名称必须是唯一的 . 已定义属性名称“TenantId” .
- 将TPT更改为TPH映射,即删除了两个
ToTable
调用 . 但它抛出同样的例外 .
我看到两个解决方法:
- 在
Customer
类中引入SalesPersonTenantId
:
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
之外,我将在 Customers
表中有一个新列 SalesPersonTenantId
. 此列是多余的,因为两个列始终必须具有来自业务角度的相同值 .
- 放弃继承映射并在
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;
}
}
}
}
}
Question: Is there any way to map the database schema above to a class model with Entity Framework?
3 回答
我认为使用Table - > TableId(PK) - >其他列(包括FK)更简单并降低复杂性 .
因此,在您的示例中 - 将CustomerId列添加到Customers表将解决您的问题 .
好吧,我似乎无法对任何事情发表评论,所以我将其作为答案添加 .
我在CodePlex上为这个问题创建了一个问题,希望他们能尽快调查 . 敬请关注!
http://entityframework.codeplex.com/workitem/865
CodePlex问题的结果(在此期间已经关闭)是问题中的情景不受支持,目前没有计划在不久的将来支持它 .
来自CodePlex的实体框架团队的报价:
不是解决方案,而是解决方法(*):一个不错的选择是使用单个Id列(as),通常是自动递增的,并提供数据库完整性使用外键,唯一索引等 . 使用触发器可以实现更复杂的数据完整性,因此您可能会采用这种方式,但您可能会将其留给应用程序业务逻辑级别,除非应用程序确实是以数据为中心 . 但是既然你正在使用实体框架,那么假设这不是你的情况可能是安全的......?
(*)如ivowiblo所建议的那样