具有抽象类的 EF4 TPT - 定义关系

发布于 2024-10-03 19:37:14 字数 2456 浏览 5 评论 0原文

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

设置

在我的网络应用程序中,我希望用户能够使用不同的身份验证机制来访问他们的帐户。除了通常的用户/密码之外,我还想让他们能够使用 Google 的 OpenId、Yahoo 的 OpenId,甚至 Facebook。这似乎很容易映射到类中:一个抽象的Account,其中有几个从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 + 代码优先 + 自定义映射的内容后,我陷入了永久陷入困境的地步,需要在正确的方向上踢一脚,然后才能再次开始:-)

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

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!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

娇女薄笑 2024-10-10 19:37:14

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

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

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

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

您的继承实现是每个具体类型的表或 TPC 继承不是 TPT,它来自于您将 Account 类设为抽象您所看到的 Account 和 OpenIdAccount 之间不存在一对一关系的情况正是 EF 在 TPC 映射时的默认行为。
如果您从 Account 类中删除抽象关键字,那么您将拥有一个 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 撰写的这篇精彩文章:

如何选择继承策略

No worries, you are in right place :) Let's get into your questions:

(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?)

That's exactly how it designed to work. In the Seed method, you would have to call SaveChanges after you add the new objects to their respective DbSets. So you are good in there.

In the database, EF didn't define any relationships between Account and OpenIdAccount.

Your inheritance implementation is Table per Concrete Type or TPC inheritance and NOT TPT and it coming from the fact that you make your Account class to be abstract and what you see in terms of not having a one to one relationship between Account and OpenIdAccount is the exact default behavior of EF when it comes to TPC mapping.
If you remove the abstract keyword from the Account class, then you would have a TPT and your code will work just fine.

So does that mean you should give up with your TPC and turn it to be a TPT? Well, that's of course one solution but you don't have to go for it if you still like to keep your TPC since it is absolutely possible to have TPC with Code First and we just need to make some slight changes to your model to make it work.

Solution:

TPC means “Create a completely separate table for each non-abstract type in my hierarchy”. Note that because there is no foreign key between the two tables we need to take care of providing unique keys, therefore we have to switch off identity on the primary key property. There are 2 ways for that:

1. By using DataAnnotations from 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. By using FluentAPI:

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

After you switch it off through one of the above ways, you would see that the exception you are getting goes away and the model starts working.

Also to make it really TPC, you should map everything in each table because in TPC there is a table for each class, and each of those tables has a column for every property of that type:

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

All that being said, I think TPC is not meant to use in this scenario and you should use TPT. For a more detailed discussion on this topic, you can check out this excellent post by Alex James:

How to choose an Inheritance Strategy

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文