2

I've been going around in circles with this and don't seem to be able to google the right answers - even after hours spent on this, so you're my last resort!

Setup

In my web app I would like to enable users to use different authentication mechanisms to access their accounts. In addition to the usual user/password thing I'd like to enable them to use Google's OpenId, Yahoo's OpenId, or even Facebook. This seems rather straightforward to map into classes: an abstract Account with several <something>Account classes inheriting some basic properties from Account. I started with the following two classes:

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; }
}

Being a bit of a perfectionist, and doing a lot of db dev in my day job, I decided table per type (TPT) would be the most desirable option. As EF4 uses TPH by default, on the DbContext side of things I defined:

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");
    }
}

Now, I wished to start with some simple tests, and thought that seeding the database on each run was a good way to have consistent data to work with:

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();
}

(Not sure why but DbContext didn't seem to ever try populate my db with the above data until I exlicitly called .SaveChanges()... any ideas?)

The Problems

One

In the database, EF didn't define any relationships between Account and OpenIdAccount. This was the first warning sign something was not right; surely OpenIdAccount should have its Id defined as both a PK and a FK pointing at Account.Id?

Two

I get the following UpdateException when .net tries to execute .SaveChanges():

A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple store-generated columns.

Next steps

Today is my first day with EF4 and code first development. Having spent numerous hours reading about EF4 + code-first + custom mapping I came to a point where I'm permanently stuck and need a kick in the right direction before I can get going again :-)

So I'm hoping that you guys can make sense of the above and explain some silly mistake / misunderstanding on my part!

4

1 回答 1

4

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

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

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

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

您的继承实现是Table per Concrete Type 或 TPC 继承,而不是TPT,它来自于您将 Account 类设为抽象的事实,并且您在 Account 和 OpenIdAccount 之间没有一对一关系时所看到的是确切的EF 在TPC映射方面的默认行为。如果您从 Account 类中删除 abstract 关键字,那么您将拥有一个TPT,并且您的代码将正常工作。

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

解决方案:

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

1. 通过使用来自的 DataAnnotations System.ComponentModel.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的这篇优秀文章:
如何选择继承策略

于 2010-11-22T00:27:32.893 回答