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

心配はいりません、あなたは正しい場所にいます:)あなたの質問に入りましょう:

(理由はわかりませんが、.SaveChanges()...を明示的に呼び出すまで、DbContextは上記のデータをデータベースに入力しようとしなかったようです...何かアイデアはありますか?)

それがまさにそれが機能するように設計された方法です。Seedメソッドでは、新しいオブジェクトをそれぞれのDbSetに追加した後、SaveChangesを呼び出す必要があります。だからあなたはそこに良いです。

データベースでは、EFはAccountとOpenIdAccountの間の関係を定義していません。

継承の実装は、コンクリートタイプごとのテーブルまたはTPC継承であり、TPTではありません。これは、Accountクラスを抽象化し、AccountとOpenIdAccountの間に1対1の関係がないという点で見られるものが正確であるという事実に由来します。 TPCマッピングに関するEFのデフォルトの動作。Accountクラスからabstractキーワードを削除すると、TPTが作成され、コードは正常に機能します。

つまり、TPCをあきらめて、TPTに変える必要があるということですか?もちろん、これは1つの解決策ですが、コードファーストでTPCを使用することは絶対に可能であり、モデルにわずかな変更を加えるだけでよいため、TPCを維持したい場合はそれを選択する必要はありません。それは動作します。

解決:

TPCは、「階層内の非抽象型ごとに完全に別個のテーブルを作成する」ことを意味します。2つのテーブルの間に外部キーがないため、一意のキーを提供する必要があることに注意してください。したがって、主キープロパティのIDをオフにする必要があります。そのための2つの方法があります:

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 に答える