首页 文章

继承和复合外键 - 基类中键的一部分,派生类中的另一部分

提问于
浏览
17

我在为以下示例数据库模式(在SQL Server中)创建实体框架代码优先映射时遇到问题:

Database schema with composite foreign keys

每个表都包含 TenantId ,它是所有(复合)主键和外键(多租户)的一部分 .

CompanyCustomerSupplier ,我尝试通过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");

基表 CompaniesAddress 具有一对多的关系(每个公司都有一个地址,无论是客户还是供应商),我可以为此关联创建映射:

modelBuilder.Entity<Company>()
     .HasRequired(c => c.Address)
     .WithMany()
     .HasForeignKey(c => new { c.TenantId, c.AddressId });

外键由主键的一部分 TenantId 和一个单独的列 AddressId 组成 . 这有效 .

正如您在数据库模式中看到的那样,从数据库的角度来看, CustomerPerson 之间的关系基本上与 CompanyAddress 之间的一对多关系相同 - 外键再次由 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 中的列组成) .

我尝试了两次修改以使其正常工作:

  • CustomerPerson 之间的外键关联更改为独立关联,即删除了属性 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 . 此列是多余的,因为两个列始终必须具有来自业务角度的相同值 .

  • 放弃继承映射并在 CompanyCustomer 之间以及 CompanySupplier 之间创建一对一映射 . 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 回答

  • 0

    我认为使用Table - > TableId(PK) - >其他列(包括FK)更简单并降低复杂性 .

    因此,在您的示例中 - 将CustomerId列添加到Customers表将解决您的问题 .

  • 0

    好吧,我似乎无法对任何事情发表评论,所以我将其作为答案添加 .

    我在CodePlex上为这个问题创建了一个问题,希望他们能尽快调查 . 敬请关注!

    http://entityframework.codeplex.com/workitem/865


    CodePlex问题的结果(在此期间已经关闭)是问题中的情景不受支持,目前没有计划在不久的将来支持它 .

    来自CodePlex的实体框架团队的报价:

    这是一个更基本的限制的一部分,其中EF不支持在基类型中定义属性,然后将其用作派生类型中的外键 . 不幸的是,这是一个很难从我们的代码库中删除的限制 . 鉴于我们没有看到很多请求,这不是我们计划在这个阶段解决的问题所以我们正在关闭这个问题 .

  • 7

    不是解决方案,而是解决方法(*):一个不错的选择是使用单个Id列(as),通常是自动递增的,并提供数据库完整性使用外键,唯一索引等 . 使用触发器可以实现更复杂的数据完整性,因此您可能会采用这种方式,但您可能会将其留给应用程序业务逻辑级别,除非应用程序确实是以数据为中心 . 但是既然你正在使用实体框架,那么假设这不是你的情况可能是安全的......?

    (*)如ivowiblo所建议的那样

相关问题