通过Fluent API在EF Code First中实现零或一到零或一的关系

41

我有两个POCO类:

订单类:

public class Order
{
    public int Id { get; set; }
    public int? QuotationId { get; set; }
    public virtual Quotation Quotation { get; set; }
    ....
}

报价类:

public class Quotation
{
    public int Id { get; set; } 
    public virtual Order Order { get; set; }
    ....   
}
  • 每个订单可以从一个或零个报价中创建,且
  • 每个报价可以导致一个订单。

因此,我有一个“一个或零”到“一个或零”的关系,如何在EF Code First中通过Fluent API实现?


1
公共的虚拟属性Quotation { get; set; },不是吗?为什么不使用属性呢?为什么所有的字段都是私有的? - ta.speot.is
抱歉,我修改了我的代码以适应属性。我的类不是虚拟的。 - Masoud
这不是一个虚拟类,而是在Order类中的一个虚拟导航属性。 - Raphaël Althaus
为了清晰起见,您可以使用此链接:https://dev59.com/0GMl5IYBdhLWcg3woYTa#45182785 - pampi
7个回答

41

通过将pocos更改为:

public class Order
{
    public int OrderId { get; set; }
    public virtual Quotation Quotation { get; set; }
}
public class Quotation
{
    public int QuotationId { get; set; }
    public virtual Order Order { get; set; }
}

并使用这些映射文件:

public class OrderMap : EntityTypeConfiguration<Order>
{
    public OrderMap()
    {
        this.HasOptional(x => x.Quotation)
            .WithOptionalPrincipal()
            .Map(x => x.MapKey("OrderId"));
    }
}
 
public class QuotationMap : EntityTypeConfiguration<Quotation>
{
    public QuotationMap()
    {
        this.HasOptional(x => x.Order)
            .WithOptionalPrincipal()
            .Map(x => x.MapKey("QuotationId"));
    }
}

我们将拥有这个数据库(也就是0..1-0..1):

enter image description here

特别感谢 (Vahid Nasiri)


2
两个配置都有 WithOptionalPrincipal 吗? - VansFannel
6
注意:你需要同时设置两个外键。如果你只设置了“Order.Quotation”,那么在数据库中只会设置“Qotations.OrderId”,而不会设置“Order.QuotationId”。 - Gert Arnold
1
我尝试了这个,但出现了错误:'在类型'SQLiteTest.Entities.ObjectB'上声明的导航属性'ObjectA'已经配置了冲突的外键。'!? - BerndK
1
在EF Core中,这个关系的替代方案是什么? - Karthic G
1
这并不是一个真正的0..1到0..1的关联,而是两个独立的这样的关联。这是因为没有任何保证如果A -> B那么B也-> A。该模型可能会说一个报价单有一个订单,并且该订单有一个不同的报价单。(例如:报价单1指向订单1,但订单1指向报价单2。) - Dave Cousineau

30

@Masoud的步骤是:

modelBuilder.Entity<Order>()
            .HasOptional(o => o.Quotation)
            .WithOptionalPrincipal()
            .Map(o => o.MapKey("OrderId"));

modelBuilder.Entity<Quotation>()
            .HasOptional(o => o.Order)
            .WithOptionalPrincipal()
            .Map(o => o.MapKey("QuotationId"));

它提供了:

enter image description here

通过将代码更改为:

modelBuilder.Entity<Order>()
            .HasOptional(o => o.Quotation)
            .WithOptionalPrincipal(o=> o.Order);

它给出:

输入图像描述


Masoud的解决方案对我来说没有正常工作 - 这个解决方案似乎是正确的。 - Ciaran Gallagher
如何使用DataAnnotations实现这个功能? - RenanStr
它如何在不定义任何键的情况下知道如何关联它们? - xr280xr

8
请参见http://msdn.microsoft.com/en-us/data/jj591620 EF Relationships。
这是一本优秀的书籍:http://my.safaribooksonline.com/book/-/9781449317867
这是一篇来自2010年12月开发者的帖子,但仍然相关:http://social.msdn.microsoft.com/Forums/uk/adonetefx/thread/aed3b3f5-c150-4131-a686-1bf547a68804。上面的文章很好地总结了可能的组合。
一个从主表获取键的依赖表解决方案是可行的。
如果您想要独立的键,在PK / FK情况下,我认为您不能使用Fluent API在Code First中实现它。 如果它们共享一个键,则可以。
1:1可选假定从属项使用主键。
但由于您需要先保存其中一个表格,因此您可以使用代码检查其中一个外键。 或在Code First创建后将第二个外键添加到数据库中。
您会接近。 但是,如果您希望两者都是外键,则EF将抱怨冲突的外键。 实际上,A依赖于B依赖于A,即使列可为空并在DB上技术上可行,EF也不喜欢。
请使用此测试程序尝试它。 只需注释Fluent API的选项即可尝试一些选项。 我无法让EF5.0与独立PK / FK 0:1到0:1配合使用,但当然有合理的妥协方案,如上所述。
using System.Data.Entity;
using System.Linq;
namespace EF_DEMO
{
class Program
{
    static void Main(string[] args) {
        var ctx = new DemoContext();
        var ord =  ctx.Orders.FirstOrDefault();
        //. DB should be there now...
    }
}
public class Order
{
public int Id {get;set;}
public string Code {get;set;}
public int? QuotationId { get; set; }   //optional  since it is nullable
public virtual Quotation Quotation { get; set; }
  //....
}
public class Quotation
{
 public int Id {get;set;}
 public string Code{get;set;}
// public int? OrderId { get; set; }   //optional  since it is nullable
 public virtual Order Order { get; set; }
 //...
}
public class DemoContext : DbContext
{
    static DemoContext()
    {
    Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DemoContext>());
    }
    public DemoContext()
        : base("Name=Demo") { }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Quotation> Quotations { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       modelBuilder.Entity<Order>().HasKey(t => t.Id)
                    .HasOptional(t => t.Quotation)
                    .WithOptionalPrincipal(d => d.Order)
                    .Map(t => t.MapKey("OrderId"));  // declaring here  via MAP means NOT declared in POCO
        modelBuilder.Entity<Quotation>().HasKey(t => t.Id)
                    .HasOptional(q => q.Order)
            // .WithOptionalPrincipal(p => p.Quotation)  //as both Principals
            //        .WithOptionalDependent(p => p.Quotation) // as the dependent
            //         .Map(t => t.MapKey("QuotationId"));    done in POCO.
            ;
    }   
}
}

谢谢,你的答案帮助了我,我找到了正确的答案并发布了它。 - Masoud

5

参考这个答案,尝试以下方法。

首先,修复你的类:

public class Order
{
  public int Id {get; set;}
  public virtual Quotation Quotation { get; set; }
  // other properties
}

public class Quotation
{
  public int Id {get; set;}
  public virtual Order Order { get; set; }
  // other properties
}

然后可以像这样使用流畅的API:
modelBuilder.Entity<Quotation>()
.HasOptional(quote => quote.Order)
.WithRequired(order=> order.Quotation);

基本上,对于1:1或[0/1]:[0/1]关系,EF需要共享主键。

谢谢,但是通过这种映射,我们将会得到一个零或一对一的关系。我找到了正确的答案并发布了它。 - Masoud

1
public class OfficeAssignment
{
    [Key]
    [ForeignKey("Instructor")]
    public int InstructorID { get; set; }
    [StringLength(50)]
    [Display(Name = "Office Location")]
    public string Location { get; set; }

    public virtual Instructor Instructor { get; set; }
}

The Key Attribute

There's a one-to-zero-or-one relationship between the Instructor and the OfficeAssignment entities. An office assignment only exists in relation to the instructor it's assigned to, and therefore its primary key is also its foreign key to the Instructor entity. But the Entity Framework can't automatically recognize InstructorID as the primary key of this entity because its name doesn't follow the ID or classnameID naming convention. Therefore, the Key attribute is used to identify it as the key:

https://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application


0

(请注意,这是使用 EF 6.4.4。)

只要不需要外键属性,指定就相当简单:

modelBuilder
.Entity<Order>()
.HasOptional(o => o.Quotation)
.WithOptionalPrincipal(q => q.Order);

modelBuilder
.Entity<Quotation>()
.HasOptional(q => q.Order)
.WithOptionalDependent(o => o.Quotation);

请注意,这里同时使用了 WithOptionalPrincipalWithOptionalDependent。这样应该会在从属侧(例如示例中的引用)上给你一个单独的外键列,但没有外键属性。如果你想在另一侧使用外键,请交换“Dependent”和“Principal”。

(请注意,并不需要同时拥有上面两个定义;WithOptionalDependent 将意味着另一侧是主体,反之亦然,因此如果你愿意,可以只使用其中一个,但我发现从两侧指定关系有助于防止重复声明而导致错误;任何冲突都将导致模型错误,以让你知道你漏掉了什么。)

虽然外键列上有一个索引,但该索引没有唯一约束。尽管可能可以添加自己的唯一约束(这将需要一个 Key IS NOT NULL 过滤器),但似乎并不起作用,在某些情况下更新关系时会出现异常。我认为这与 “交换问题” 有关,其中 EF 将执行其更新操作来分别查询,因此强制唯一性将阻止 EF 通过两步 “移动” 键。

EF 似乎在内部处理关联关系,而没有唯一的 DB 约束:

  • 在任一侧,分配已使用的引用会自动删除引用的其他用法。(因此,如果在打开上下文时已经存在A1 <=> B1的情况,然后您写入A1 => B2,则A1 <=> B1将被删除,而A1 <=> B2将被添加,无论您在哪一侧。)
  • 如果您尝试通过多次分配相同的引用来创建重复键,则EF将抛出异常,显示“多重性约束违规”。(因此,在同一上下文中,您编写了A1 => B1和A2 => B1,或某些类似的冲突映射。)
  • 如果您手动更新DB以创建重复键情况,则当EF遇到此情况时,它将抛出异常,显示“发生关系多重性约束违规...这是一个不可恢复的错误。”

在EF6中似乎不可能将属性映射到外键列(至少使用Fluent API)。尝试这样做会导致非唯一列名异常,因为它尝试为属性和关联分别使用相同的名称。

请注意,技术上来说,有两个外键(即:双向各一个)是不正确的。这样的安排实际上将是 两个 0..1 到 0..1 的关联,因为没有什么可以说明两端的键应该匹配。如果您通过 UI 和/或可能是某种数据库约束来强制执行关系,则可能会起作用。
我还注意到可能存在对 0..1 到 0..1 关联的理解误解/沟通问题。从我的理解和 EF 看待它的方式来看,这意味着它是一个可选的双向 1 对 1 关联。因此,您可以在任一侧拥有没有关系的对象。(而对于 1 对 0..1 关联,一侧的对象可以存在而没有关系,但另一侧的对象始终需要一个对象来关联。)

但是0..1到0..1并不意味着您可以让关联只在一个方向上进行而不是另一个方向。如果A1 => B1,则B1 => A1(A1 <=> B1)。您不能将B1分配给A1而不使A1与B1相关联。这就是为什么这个关联只使用单个外键的原因。我认为有些人可能试图建立一个关联,其中这种情况不成立(A1与B1相关联,但B1不与A1相关联)。但那实际上不是一个关联,而是两个0..1到0..1的关联。


这和这个答案以及其他两个答案完全一样。再说一遍的原因是什么? - Gert Arnold
@GertArnold 这并不完全相同,它非常具体地不同。那个答案错误地创建了两个关联。这是唯一一个正确展示如何使用流畅API创建单个0..1到0..1关联(并解释这些类型关联的细节)的答案。 - Dave Cousineau
@GertArnold,实际上我看到你链接了Kenneth的答案而不是Masoud的。他的代码类似,但没有解释,也没有人指出一个关联的一侧应该是从属方,另一侧应该是主体方,以及这意味着什么。这是唯一一个不仅概述了关联的完整规范(使用流畅的API),还解释了其中的细节,例如如果您想要外键属性,则此方法将无法工作。(除了记录我自己的试错结果之外,我不会回答这个问题,尽管这个问题的答案不正确或不完整。) - Dave Cousineau
让我们在聊天中继续这个讨论 - Dave Cousineau
我对尝试多个答案中发现的所有变体有些迷失方向(我会删除我的评论),但事实仍然存在,即EF6无法创建正确的模型。在数据库级别上,它是1:n。这一切都与我的主要观点无关:它重复了一个已经存在的答案。 - Gert Arnold
@GertArnold,我现在明白为什么没有唯一约束:EF将会在两个步骤中“移动”一个键;如果存在唯一约束,则“移动”(交换)一个键只能在一步中完成。(我不知道你是如何声称这个答案是多余的,因为它现在是关于此主题最详尽的答案之一,无论在哪里。) - Dave Cousineau

0

使用DataAnnotations:

public class Order
{
       [Key]
       public int Id {get; set;}

       public virtual Quotation Quotation { get; set; }
}

public class Quotation
{
     [Key, ForeignKey(nameof(Order))]
     public int Id {get; set;}

     public virtual Order Order { get; set; }
}

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