使用Entity Framework Fluent API实现一对一可选关系

81
我们希望使用Entity Framework Code First实现一对一可选关系。我们有两个实体。
public class PIIUser
{
    public int Id { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

PIIUser 可能有一个 LoyaltyUserDetail,但是 LoyaltyUserDetail 必须有一个 PIIUser。我们尝试了这些流畅的方法技巧。

modelBuilder.Entity<PIIUser>()
            .HasOptional(t => t.LoyaltyUserDetail)
            .WithOptionalPrincipal(t => t.PIIUser)
            .WillCascadeOnDelete(true);

这种方法没有在PIIUsers表中创建LoyaltyUserDetailId外键。

之后我们尝试了以下代码。

modelBuilder.Entity<LoyaltyUserDetail>()
            .HasRequired(t => t.PIIUser)
            .WithRequiredDependent(t => t.LoyaltyUserDetail);

但是这一次EF没有在这两个表中创建任何外键。

您对此问题有什么想法吗? 我们如何使用实体框架流畅的API创建一个一对一可选关系?

7个回答

105

EF Code First支持1:11:0..1关系,后者是您正在寻找的(“一对零或一”)。

您的尝试中流利地表达了两端都必须,在另一个案例中则是两端都可选

您需要的是一端可选,而另一端必须

以下是来自《Programming E.F. Code First》书籍的示例:

modelBuilder.Entity<PersonPhoto>()
.HasRequired(p => p.PhotoOf)
.WithOptional(p => p.Photo);

PersonPhoto实体有一个导航属性称为PhotoOf,它指向Person类型。 Person类型有一个导航属性称为Photo,它指向PersonPhoto类型。

在这两个相关类中,您使用每个类型的主键而不是外键。也就是说,您不会使用LoyaltyUserDetailIdPIIUserId属性。相反,关系依赖于两种类型的Id字段。

如果您像上面那样使用流畅API,则不需要将LoyaltyUser.Id指定为外键,EF会自动找到它。

因此,没有您的代码供我自己测试(我讨厌凭记忆做这个)... 我将将其翻译为以下代码:

public class PIIUser
{
    public int Id { get; set; }    
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }    
    public PIIUser PIIUser { get; set; }
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<LoyaltyUserDetail>()
  .HasRequired(lu => lu.PIIUser )
  .WithOptional(pi => pi.LoyaltyUserDetail );
}

这句话的意思是LoyaltyUserDetails中的PIIUser属性是必需的,而PIIUser的LoyaltyUserDetail属性是可选的。

你也可以从另一个方面开始:

modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

现在,LoyaltyUserDetail属性是可选的,而PIIUser属性是必需的。

您始终需要使用模式HAS/WITH。

希望对您有所帮助,并且顺带一提,一对一(或一对零/一)关系是在代码优先配置中最令人困惑的关系之一,因此您并不孤单! :)


1
这难道不会限制用户选择哪个FK字段吗?(我认为他想要这样,但他还没有回复,所以我不知道),因为他似乎想在类中有一个FK字段。 - Frans Bouma
1
同意。这是EF工作方式的限制。1:1和1:0..1依赖于主键。否则,我认为你会偏离“唯一FK”,这在EF中仍不受支持。:((你更像一个数据库专家...这正确吗...这实际上是关于唯一FK吗?)并且根据http://entityframework.codeplex.com/workitem/299,它不会出现在即将到来的Ef6中。 - Julie Lerman
1
是的@FransBouma,我们想要使用PIIUserId和LoyaltUserId字段作为外键。但正如你和Julie所提到的,EF在这种情况下限制了我们。感谢你们的回复。 - Ilkay Ilknur
1
WithOptional指向一个可选的关系,例如0..1(零或一),因为OP说“PIIUser可能有LoyaltyUserDetail”。你混淆了术语导致了第二个问题。;)那些是WithRequiredDependent和WithRequiredPrincipal。这样更有意义吗?指向一个必需的端点,可以是从属端(也称为“子级”)或主要端(也称为“父级”)。即使在一个一对一的关系中,两者都是相等的,EF也需要将一个称为主要的,另一个称为从属的。希望对你有所帮助! - Julie Lerman
谢谢你的回答。它帮助我理解了从1到0或从0到1的映射。 - Thomas.Benz
显示剩余5条评论

29

如果在LoyaltyUserDetailPIIUser之间存在一对多的关系,那么你应该这样进行映射

modelBuilder.Entity<LoyaltyUserDetail>()
       .HasRequired(m => m.PIIUser )
       .WithMany()
       .HasForeignKey(c => c.LoyaltyUserDetailId);

EF 应该创建所有需要的外键,只需要 不关心 WithMany

3
Julie Lerman的答案被接受了(并且应该保持接受状态,依我所见),因为它回答了问题,并详细解释了为什么这是正确的方式,提供了支持性的证据和讨论。在Stack Overflow上肯定可以找到复制粘贴的答案,我自己也用过几个,但作为一名专业开发人员,你应该更关心如何成为一个更好的程序员,而不仅仅是编译代码。话虽如此,jr也说对了,虽然晚了一年。 - Mike Devenney
1
@ClickOk 我知道这是一个老问题和评论,但这不是正确的答案,因为它使用了一对多作为解决方法,这并没有建立一对一或一对零的关系。 - Mahmoud Darwish

3
public class User
{
    public int Id { get; set; }
    public int? LoyaltyUserId { get; set; }
    public virtual LoyaltyUser LoyaltyUser { get; set; }
}

public class LoyaltyUser
{
    public int Id { get; set; }
    public virtual User MainUser { get; set; }
}

        modelBuilder.Entity<User>()
            .HasOptional(x => x.LoyaltyUser)
            .WithOptionalDependent(c => c.MainUser)
            .WillCascadeOnDelete(false);

这将解决在引用外键上的问题。
更新删除记录时。

这样做不会按预期工作。它会生成迁移代码,该代码不使用LoyaltyUserId作为外键,因此User.Id和LoyaltyUser.Id将具有相同的值,并且LoyaltyUserId将保持为空。 - Aaron Queenan

3
你的代码有几个问题。
一个1:1关系是指: PK<-PK,其中一个PK端也是FK,或者PK<-FK+UC,其中FK端是非PK并且有一个UC。你的代码显示你有FK<-FK,因为你定义了两个端都有一个FK,但是这是错误的。我认为PIIUser是PK端,LoyaltyUserDetail是FK端。这意味着PIIUser没有FK字段,但LoyaltyUserDetail有。
如果1:1关系是可选的,则FK端必须至少有1个可空字段。 p.s.w.g.回答了你的问题,但他/她犯了一个错误,也在PIIUser中定义了一个FK,这当然是错误的,如上所述。因此,在LoyaltyUserDetail中定义可空的FK字段,在LoyaltyUserDetail中定义属性以标记它为FK字段,但不要在PIIUser中指定FK字段。
你会得到上面描述的异常,因为没有一侧是PK端(主端)。 EF对于1:1关系不是很好,因为它无法处理唯一约束。我不是Code First的专家,所以不知道它是否能够创建UC。
(编辑)顺便说一下:A1:1B(FK)意味着只创建一个FK约束,指向B的目标指向A的PK,而不是2个。

2
尝试将 LoyaltyUserDetail 属性添加 ForeignKey 属性:
public class PIIUser
{
    ...
    public int? LoyaltyUserDetailId { get; set; }
    [ForeignKey("LoyaltyUserDetailId")]
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
    ...
}

还有PIIUser属性:

public class LoyaltyUserDetail
{
    ...
    public int PIIUserId { get; set; }
    [ForeignKey("PIIUserId")]
    public PIIUser PIIUser { get; set; }
    ...
}

1
我们之前尝试过数据注解,但是它并没有起作用。如果你按照上面提到的方法添加了数据注解,在使用EF时会抛出如下异常:无法确定 'LoyaltyUserDetail' 和 'PIIUser' 之间关联的主端。必须使用关系流畅API或数据注解明确配置此关联的主端。因此我们放弃了数据注解,转而采用流畅API方法。 - Ilkay Ilknur
@İlkayİlknur 如果您只在关系的一个端点上添加 ForeignKey 属性会发生什么?即仅在 PIIUserLoyaltyUserDetail 上。 - p.s.w.g
Ef 抛出相同的异常。 - Ilkay Ilknur
1
@p.s.w.g 为什么在 PIIUser 上使用 FK?LoyaltyUserDetail 有一个 FK,而不是 PIIUser。因此,在 LoyaltyUserDetail 的 PIIUserId 属性上必须是[Key,ForeignKey("PIIUser")]。请尝试这个链接:http://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-first.aspx - user808128
@user808128 可以将该注解放置在导航属性或ID上,没有区别。 - Thomas Boby
显示剩余2条评论

2

对于任何仍在使用EF6且需要将外键与主键不同的人来说,这对原帖毫无用处,以下是如何实现:

public class PIIUser
{
    public int Id { get; set; }

    //public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    public int Id { get; set; }
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

modelBuilder.Entity<PIIUser>()
    .HasRequired(t => t.LoyaltyUserDetail)
    .WithOptional(t => t.PIIUser)
    .Map(m => m.MapKey("LoyaltyUserDetailId"));

请注意,您不能使用LoyaltyUserDetailId字段,因为据我所知,它只能使用流畅API来指定。(我已经尝试了三种使用ForeignKey属性的方法,但都没有成功)。


这应该是被接受的答案 :) - Shadam

1
上述解决方案中令人困惑的一点是,两个表中的主键都被定义为“Id”,如果您基于表名创建了主键,则无法正常工作。我已经修改了类以说明相同的问题,即可选表不应定义自己的主键,而应该使用主表中相同的键名。
public class PIIUser
{
    // For illustration purpose I have named the PK as PIIUserId instead of Id
    // public int Id { get; set; }
    public int PIIUserId { get; set; }

    public int? LoyaltyUserDetailId { get; set; }
    public LoyaltyUserDetail LoyaltyUserDetail { get; set; }
}

public class LoyaltyUserDetail
{
    // Note: You cannot define a new Primary key separately as it would create one to many relationship
    // public int LoyaltyUserDetailId { get; set; }

    // Instead you would reuse the PIIUserId from the primary table, and you can mark this as Primary Key as well as foreign key to PIIUser table
    public int PIIUserId { get; set; } 
    public double? AvailablePoints { get; set; }

    public int PIIUserId { get; set; }
    public PIIUser PIIUser { get; set; }
}

然后跟着


modelBuilder.Entity<PIIUser>()
.HasOptional(pi => pi.LoyaltyUserDetail)
.WithRequired(lu => lu.PIIUser);

这个方案可以解决问题,但是被接受的解决方案没有清晰地解释这一点,让我花费了几个小时来找出原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接