【问题标题】:Is it possible to capture a 0..1 to 0..1 relationship in Entity Framework?是否可以在实体框架中捕获 0..1 到 0..1 的关系?
【发布时间】:2021-02-22 04:47:32
【问题描述】:

有没有办法为实体框架中的可空外键关系创建可空反向导航属性?在数据库用语中,0..1 到 0..1 关系。

我已经尝试如下,但我不断收到错误消息:

无法确定类型“Type1”和“Type2”之间关联的主体端。此关联的主体端必须使用关系流式 API 或数据注释显式配置。

public class Type1 {

    public int ID { get; set; }

    public int? Type2ID { get; set; }
    public Type2 Type2 { get; set; }
}

public class Type2 {

    public int ID { get; set; }

    public int? Type1ID { get; set; }
    public Type1 Type1 { get; set; }
}

我知道整数列只能存在于一个表或另一个表中,但肯定应该可以涵盖所有必要的情况吗?例如:

Type1      Type2
========   ===============
ID         ID   | Type1ID
--------   ---------------
1          1    | null
2          2    | 2

我尝试过使用数据注释(例如[ForeignKey] 一端,[InverseProperty] 两端),但这些似乎都无济于事。

如果可能,数据注释解决方案将优于 Fluent API。此外,如果有帮助的话,从域的角度来看,int? 属性对于任何一个类都不是绝对必要的。

有一个有趣的解决方法here 这意味着不可能在实体框架中捕获这种关系(实际上,一个项目是可选的集合的一部分) - 如果是这样,是否有任何文档那会支持这个吗?。

【问题讨论】:

    标签: c# entity-framework foreign-keys


    【解决方案1】:

    在 EF6 和更早版本中,正确实现这样的关联并不是那么容易。幸运的是,EF-core 在支持的关联方面有了很大的改进。现在,实现通过数据库约束强制执行这种关联的唯一模型是小菜一碟。即:Car 和 Driver 之间的连接类,其中外键具有唯一索引(下面的选项 4)。它甚至几乎完全适用于默认映射约定。

    型号:

    class Car
    {
        public int ID { get; set; }
        public string Brand { get; set; }
        public CarDriver CarDriver { get; set; }
    }
    
    class Driver
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public CarDriver CarDriver { get; set; }
    }
    
    class CarDriver
    {
        public int CarId { get; set; }
        public int DriverId { get; set; }
        public Car Car { get; set; }
        public Driver Driver { get; set; }
    }
    

    唯一需要的显式映射:

    class CarDriverConfig : IEntityTypeConfiguration<CarDriver>
    {
        public void Configure(EntityTypeBuilder<CarDriver> builder)
        {
            builder.HasKey(cd => new { cd.CarId, cd.DriverId });
        }
    }
    

    这就是 EF 创建正确数据库模型所需的全部内容:

    CREATE TABLE [Car] (
      [ID] int NOT NULL IDENTITY,
      [Brand] nvarchar(max) NULL,
      CONSTRAINT [PK_Car] PRIMARY KEY ([ID])
    );
    CREATE TABLE [Driver] (
      [ID] int NOT NULL IDENTITY,
      [Name] nvarchar(max) NULL,
      CONSTRAINT [PK_Driver] PRIMARY KEY ([ID])
    );
    CREATE TABLE [CarDriver] (
      [CarId] int NOT NULL,
      [DriverId] int NOT NULL,
      CONSTRAINT [PK_CarDriver] PRIMARY KEY ([CarId], [DriverId]),
      CONSTRAINT [FK_CarDriver_Car_CarId] FOREIGN KEY ([CarId]) REFERENCES [Car] ([ID]) ON DELETE CASCADE,
      CONSTRAINT [FK_CarDriver_Driver_DriverId] FOREIGN KEY ([DriverId]) REFERENCES [Driver] ([ID]) ON DELETE CASCADE
    );
    CREATE UNIQUE INDEX [IX_CarDriver_CarId] ON [CarDriver] ([CarId]);
    CREATE UNIQUE INDEX [IX_CarDriver_DriverId] ON [CarDriver] ([DriverId]);
    

    最后这两个指标是锦上添花。它们表明 EF 完全了解这里发生的情况。


    原创,但已更新,答案

    “这不难”是我在阅读您的问题时的想法。但我再次发现一对一的关联充满了陷阱。我们开始吧。

    我假设0..1 – 0..1 的意思是两个对象可以相互独立存在,但也可以相互独占。

    让我们把它具体化。 Car 和 Driver。想象一个由许多汽车和司机组成的池子,其中包括 CarA 和 DriverA。现在假设您希望 CarA 与 DriverA 关联,并且您的实现是 DriverA 将自己链接到 CarA。但是一旦 DriverA 执行此操作,您希望 CarA 仅用于 DriverA,CarA 的关联不再是可选的,因此也应该立即设置它。

    如何实现?

    选项 1:

    如果这是工作模型:

    public class Car
    {
        public int CarId { get; set; }
        public string Name { get; set; }
        public int? DriverId { get; set; }
        public virtual Driver Driver { get; set; }
    }
    
    public class Driver
    {
        public int DriverId { get; set; }
        public string Name { get; set; }
        public int? CarId { get; set; }
        public virtual Car Car { get; set; }
    }
    

    从技术上讲,DriverA 可以具有 CarA 的外键,CarA 可以具有 DriverB 的外键。

    因此,当外键DriverA-CarA建立时,你应该“同时”建立反向外键CarA-DriverA。这是你应该在代码中做的事情,这意味着它是一个业务规则。实际上,它不是原子操作,因此您必须确保它在一个数据库事务中完成。

    类模型至少支持用例,但是太放纵了。它需要受到约束。更重要的是,它不适用于 EF。 EF 抱怨必须设置本金结束。如果这样做,EF 将不会创建双向关联。

    提出了另一种映射here。我试过了,但有两个可选的关联:

    在Driver的映射配置中:

    this.HasOptional(t => t.Car).WithMany().HasForeignKey(d => d.CarId);
    

    在Car的映射配置中:

    this.HasOptional(t => t.Driver).WithMany().HasForeignKey(c => c.DriverId);
    

    (没有数据注释替代方案)

    我发现EF在创建新司机和汽车时只在数据库中设置一个外键值。您必须分别设置和保存两个关联,管理您自己的事务。对于现有对象,您仍然需要设置两个外键,尽管这可以保存在一个 SaveChanges 调用中。

    更好的选择?让我们看看...

    选项 2:

    这是您引用的链接中提到的一对多关联。该模型需要外部约束,但创建关联是原子的。而且你在一端仍然有一个参考,在另一端有一个集合。并且它可以轻松地与 EF 进行映射。

    选项 3:

    您可以创建一个联结表CarDriver,它有两个外键Car 和Driver,这两个外键都包含其唯一的主键:

    这是一个常规的多对多关联。默认情况下,EF 会将其映射为一个类模型,其中Car 和Driver 具有相互指向的集合属性,并且不直接映射联结表:

    public class Car
    {
        public int CarId { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Driver> Drivers { get; set; }
    }
    
    public class Driver
    {
        public int DriverId { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Car> Cars { get; set; }
    }
    

    现在关联的创建是一个原子操作。完全可以用 EF 映射这个模型。相互引用已经消失,但您仍然可以获得集合属性的 FirstOrDefault() 作为代理引用。

    但是有一个重要的问题。现在每个对象都可以有任意个相关的对应对象。如果您创建关联,则需要一个编码的业务规则来检查所涉及的对象是否还没有任何关联。也许这个选项比选项 2 更糟糕。但我提到它是因为下一个选项:

    选项 4

    选项 3 是原子的,但它也需要外部约束。要使关联独占,CarDriver 中的两列都应具有唯一键,因此每辆汽车或驾驶员只能在表中出现一次。通过这些索引,模型自己实现了双向可选的 1:1 关联。任何处理它的代码都必须遵守规则。安全无恙...

    在EF6中,由于引入了HasIndex,可以通过这个映射来实现:

    modelBuilder.Entity<Car>().HasOptional(c => c.CarDriver).WithRequired();
    modelBuilder.Entity<Driver>().HasOptional(c => c.CarDriver).WithRequired();
    modelBuilder.Entity<CarDriver>().HasKey(cd => new { cd.CarId, cd.DriverId });
    modelBuilder.Entity<CarDriver>().HasIndex(cd => cd.CarId).IsUnique();
    modelBuilder.Entity<CarDriver>().HasIndex(cd => cd.DriverId).IsUnique();
    

    但是,由于 EF6 默认会在 FK 字段上添加索引,因此唯一索引会添加到默认的非唯一索引之上。所以还是需要人工干预迁移代码才能去掉后者。

    结论

    选项 1 最接近您想要的。但我不喜欢设置两个外键的义务,这很容易被未来的开发人员忘记或忽略。

    但是选项 2 和 3 在编码业务规则方面的要求甚至更高,可能会被遗忘。并且集合是不自然的,因为代理“1”结束。选项 3 对我有一些吸引力,因为 Car 和 Driver 在数据库中是完全独立的,并且关联是具有不可为空外键的记录(DBA 也倾向于这样做)。

    选项 4 具有相同的吸引力,当多个应用程序必须实现需要对选项 2 和 3 施加的外部约束时,它是最佳选项。此外,即使忘记了编码规则,数据库约束也是最后的收获。但是EF6不容易实现。

    【讨论】:

    • 感谢您的精彩回答,这是值得深思的良药!尽管存在唯一性问题,但由于操作的原子性,连接表看起来是最直接的方法。如果您也将CarDriver 设为代码端模型,我相信您也可以避免收集不是真正的集合(尽管我不确定这将如何影响独特性问题)。
    • 是的,在模型中加入CarDriver 甚至是第 5 个选项,我没有提到,因为您似乎想坚持相互引用。事实上,我几乎从未遇到过只有一个(透明的)纯连接表就足够的现实世界的多对多关联。如果您可以使用间接引用,这是一个不错的选择,还因为您可以使用外键关联(具有原始 FK 属性)而不是独立关联(仅对象引用)。​​
    • 另见:stackoverflow.com/a/14819265/861716。必须显式设置两个外键同样令人讨厌。
    • 请参阅here 以获得专注于 EF-core 的答案。
    【解决方案2】:

    这不是您使用实体框架构建表格的方式。这些类的正确声明是:

    public class Type1 {
        public int ID { get; set; }
    }
    
    public class Type2 {
    
        public int ID { get; set; }
        public virtual Type1 @Type1 { get; set; }
    }
    

    编辑: 我认为做你想做的最简单的方法是:

        public class Type1 {
            public int ID { get; set; }
            public virtual ContainerClass {get; set;}
        }
    
        public class Type2 {
    
            public int ID { get; set; }
            public virtual ContainerClass {get; set;}
        }
    
        public class ContainerClass {
             public int ID {get;set;}
             public virtual Type1 @Type1 {get;set;}
             public virtual Type2 @Type2 {get;set;}
    }
    

    【讨论】:

    • 但是我仍然想要Type1 中的反向导航属性,这样我就可以访问Type1.Type2 以及Type2.Type1。我会把标题改得更清楚一点。
    • 所以也许要走的路就是拥有一个容器类型。我将编辑我的回复来说明。
    • 这可能是最简单最清晰的解决方案。除非我同时遇到其他问题,否则我会将其标记为正确答案。
    【解决方案3】:

    凭记忆做这个,很遗憾没有测试:

    public Type1
    {
        [Key]        
        public int ID { get; set; }
    
        [ForeignKey("Type2")]
        public int? Type2ID { get; set; }
        public virtual Type2 Type2 { get; set; }
    }
    
    public Type2
    {
        [Key]        
        public int ID { get; set; }
    
        [ForeignKey("Type1")]
        public int? Type1ID { get; set; }
        public virtual Type1 Type1 { get; set; }
    }
    

    顺便说一句,在实体上使用显式 ForeignKey 允许测试是否存在关联对象,而不必调用数据库。例如IEnumerable&lt;Type1&gt;.Where(t =&gt; t.Type2ID.HasValue) 将返回 Type1 的所有具有关联 Type2 的对象!有关更多详细信息,请参阅this question。

    【讨论】:

    • 这给了我一个不同的错误:“Multiplicity is not valid in Role 'Type1_Type2_Target' in relationship 'Type1_Type2'. 因为从属角色属性不是关键属性,所以多重性的上限从属角色必须是“*”。” ...尽管这使我走上了一些可能可行的不同道路
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多