【问题标题】:EF4 TPT with abstract classes - defining relationships带有抽象类的 EF4 TPT - 定义关系
【发布时间】:2010-11-22 09:31:25
【问题描述】:

我一直在绕圈子,似乎无法在谷歌上找到正确的答案 - 即使在这上面花了几个小时,所以你是我最后的选择!

设置

在我的网络应用程序中,我想让用户使用不同的身份验证机制来访问他们的帐户。除了通常的用户/密码之外,我还想让他们能够使用 Google 的 OpenId、Yahoo 的 OpenId 甚至 Facebook。这似乎很容易映射到类中:一个抽象的Account 与几个<something>Account 类继承了Account 的一些基本属性。我从以下两个类开始:

public abstract class Account
{
    public int Id { get; set; }
    public int OwnerId { get; set; }

    public virtual Person Owner { get; set; }
}

public class OpenIdAccount : Account
{
    public string Identifier { get; set; }
}

作为一个完美主义者,并且在我的日常工作中进行了大量的数据库开发,我认为每个类型的表 (TPT) 将是最理想的选择。由于 EF4 默认使用 TPH,因此在我定义的 DbContext 方面:

public class MySampleDb : DbContext
{
    public DbSet<Person> People { get; set; }
    public DbSet<Account> Accounts { get; set; }
    public DbSet<OpenIdAccount> OpenIdAccounts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Account>().MapHierarchy(a => new
        {
            a.Id, 
            a.OwnerId
        }).ToTable("Account");

        modelBuilder.Entity<OpenIdAccount>().MapHierarchy(oid => new
        {
            oid.Id,
            oid.Identifier
        }).ToTable("OpenIdAccount");
    }
}

现在,我希望从一些简单的测试开始,并认为在每次运行时为数据库播种是获得一致数据的好方法:

protected override void Seed(MySampleDb context)
{
    Person johns = new Person
    {
        Id = 1,
        Nickname = "John Skeet"
    };

    OpenIdAccount google = new OpenIdAccount
    {
        Id = 2,
        OwnerId = 1,
        Identifier = "https://www.google.com/accounts/o8/id?id=AItOawnmUz4e6QIn9mgd98WMAbnzC25sji5lpSM"
    };

    context.People.Add(johns);
    context.Accounts.Add(google);
    context.SaveChanges();
}

(不知道为什么,但 DbContext 似乎从未尝试使用上述数据填充我的数据库,直到我明确调用 .SaveChanges()... 有什么想法?)

问题

一个

在数据库中,EF 没有定义AccountOpenIdAccount 之间的任何关系。这是出现问题的第一个警告信号。 OpenIdAccount 是否应该将其 Id 定义为指向 Account.Id 的 PK 和 FK?

两个

当 .net 尝试执行 .SaveChanges() 时,我得到以下 UpdateException

跨实体或关联共享的值 在多个位置生成。检查 映射不会将 EntityKey 拆分为多个 存储生成的列。

后续步骤

今天是我使用 EF4 和代码优先开发的第一天。花了很多时间阅读有关 EF4 + 代码优先 + 自定义映射的信息后,我到了一个永久卡住的地步,需要朝着正确的方向前进,然后才能重新开始:-)

所以我希望你们能够理解上述内容并解释我的一些愚蠢的错误/误解!

【问题讨论】:

    标签: .net entity-framework ef4-code-only


    【解决方案1】:

    不用担心,您来对地方了 :) 让我们来回答您的问题:

    (不知道为什么,但 DbContext 似乎从未尝试用上述数据填充我的数据库,直到我明确调用 .SaveChanges()...任何想法?)

    这正是它设计的工作方式。在 Seed 方法中,您必须在将新对象添加到它们各自的 DbSet 后调用 SaveChanges。所以你在那里很好。

    在数据库中,EF 没有定义 Account 和 OpenIdAccount 之间的任何关系。

    您的继承实现是 Table per Concrete Type 或 TPC 继承NOT TPT,这是因为您将 Account 类设为 abstract 以及您所看到的 Account 和 OpenIdAccount 之间没有一对一的关系是 EF 在 TPC 映射时的确切默认行为。 如果您从 Account 类中删除 abstract 关键字,那么您将有一个 TPT 并且您的代码将正常工作。

    那么这是否意味着您应该放弃您的 TPC 并将其转变为 TPT?好吧,这当然是一种解决方案,但是如果您仍然想保留 TPC,则不必这样做,因为绝对有可能使用 Code First 来使用 TPC,我们只需要对您的模型进行一些细微的更改即可这行得通。

    解决办法:

    TPC 的意思是“为我的层次结构中的每个非抽象类型创建一个完全独立的表”。请注意,由于两个表之间没有外键,我们需要注意提供唯一键,因此我们必须关闭主键属性上的标识。有两种方法:

    1.通过使用来自 System.ComponentModel.DataAnnotations的DataAnnotations:

    public abstract class Account 
    {
        [StoreGenerated(StoreGeneratedPattern.None)]
        public int Id { get; set; }
        public int OwnerId { get; set; }
        public virtual Person Owner { get; set; }
    }
    


    2.通过使用 FluentAPI:

    modelBuilder.Entity<Account>().Property(a => a.Id)
        .StoreGeneratedPattern = System.Data.Metadata.Edm.StoreGeneratedPattern.None;
    

    通过上述方式之一将其关闭后,您会看到您遇到的异常消失并且模型开始工作。

    此外,要使其真正成为 TPC,您应该映射每个表中的所有内容,因为在 TPC 中,每个类都有一个表,并且每个表对于该类型的每个属性都有一个列

    modelBuilder.Entity<Account>().MapHierarchy(a => new {
        a.Id,
        a.OwnerId,
    })
    .ToTable("Accounts");
    
    modelBuilder.Entity<OpenIdAccount>().MapHierarchy(o => new {
        o.Id,
        o.OwnerId,
        o.Identifier
    })
    .ToTable("OpenIdAccounts");
    


    话虽如此,我认为 TPC 不适合在这种情况下使用,您应该使用 TPT。有关此主题的更详细讨论,您可以查看 Alex James 的这篇精彩文章:
    How to choose an Inheritance Strategy

    【讨论】:

    • Morteza,感谢您详细说明我的策略失败的原因!我浏览了几篇关于选择继承策略的文章,并坚定地决定采用 TPT - 只是没有意识到如果我遵循这条路,我将不得不使 Account 具体而不是抽象 - 这种违反 OO 原则!无论如何,当我删除 Account 上的 abstract 关键字时,它起作用了,EF 友好地将 OpenIdAccount 表中的 Id 标记为 PK 和 FK。
    • Re SaveChanges in Seed:ScottGu 不调用 SaveChanges (weblogs.asp.net/scottgu/archive/2010/07/16/…),ADO.NET 团队也不 (blogs.msdn.com/b/adonet/archive/2010/09/02/…)。一旦我将Account 更改为具体类,Seed 就开始在没有SaveChanges() 的情况下工作 - 猜测它是隐式调用的。
    • 很抱歉发送了这么多垃圾邮件,但好奇心正在吞噬我。我阅读了几篇关于 EF 在 TPT 上的糟糕表现以及生成了一些可怕的 SQL 的文章。如果我自己做数据库,我会使用鉴别器列(例如'AccountTypeId'),并在需要时根据该信息加入正确的表。 EF 是否支持带有 TPT 的鉴别器列?或者当我拨打db.Accounts.OfType&lt;OpenIdAccount&gt;(). 时,它是否“知道”加入什么?
    • 没问题。关于 FK,是的,TPT 在层次结构中的表之间带来了 FK,这就是我说 TPT 更适合您的场景的原因之一。 SavingChanges() 非常有趣,但我在 EF 团队关于 EF CTP5 的演示文稿中看到,他们在他们的 Seed 方法中调用它,所以我猜他们将在 RTW 中强制(或至少推荐)它明年的版本。
    • 如果我理解正确的话,通过 TPT 有一个鉴别器列,你的意思是帐户表中的一个类型字段限定了该行:即使你可以通过 FluentAPI 做到这一点(不确定是否可能) , EF 不会将其与 TPT 考虑在内。当您使用 TPT 时,EF 通过查看它从您的类创建的模型来学习如何生成 SQL 语句。在数据库优先或模型优先方法中使用 EF 时也是一样的,只是模型是由 EF 而不是我们创建的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-05-15
    • 1970-01-01
    • 1970-01-01
    • 2015-03-19
    • 2010-12-15
    • 2018-07-26
    • 2011-06-24
    相关资源
    最近更新 更多