Entity Framework Core 零或一到零或一关系

Ale*_*lex 3 c# entity-framework-core ef-fluent-api

鉴于这些类:

public class A
{
    public int Id {get; set;}
    public int? BId {get; set;}
    public B B {get; set;}
}

public class B
{
    public int Id {get; set;}
    public int? AId {get; set;}
    public A A {get; set;} 
}
Run Code Online (Sandbox Code Playgroud)

然后使用 Fluent API

 modelBuilder.Entity<A>()
                .HasOne(a => a.B)
                .WithOne(b => b.A)
                .HasForeignKey<A>(a => a.BId);
Run Code Online (Sandbox Code Playgroud)

创建对象并将它们添加到数据库时,相应表中的内容如下所示:

  • [A].BId 已设置
  • [B].AId = 空

当我使用 EF Core 检索数据时:

  • AB 已设置,A.BId 已设置
  • BA 已设置,但B.AId 为 null

我应该怎么做才能设置 B.AId?

Ger*_*old 7

这些0..1 : 0..1关系通常在实体之间定义,其中没有一个是明显的主体实体。我喜欢汽车和司机的例子,这比 A 和 B 更容易想象。

您所追求的模型如下所示:

一对一密钥

有两个相互的外键,它们都有一个唯一的索引以在数据库级别强制执行 1:1。

HasOne - WithOne这里不能使用组合,因为这总是需要一个HasForeignKey指令来告诉哪个实体是主体。这也仅将一个字段配置为外键。在您的示例中,B.AId只是一个常规字段。如果你不给它一个值,EF 也不会。

上述模型的映射比HasOne - WithOne

var carEtb = modelBuilder.Entity<Car>();
var driverEtb = modelBuilder.Entity<Driver>();

carEtb.HasOne(c => c.Driver).WithMany();
carEtb.HasIndex(c => c.DriverID).IsUnique();

driverEtb.HasOne(d => d.Car).WithMany();
driverEtb.HasIndex(c => c.CarID).IsUnique();
Run Code Online (Sandbox Code Playgroud)

所以有两个 0..1:n 关联,它们通过外键上的索引变得唯一。

它创建了以下数据库模型:

var carEtb = modelBuilder.Entity<Car>();
var driverEtb = modelBuilder.Entity<Driver>();

carEtb.HasOne(c => c.Driver).WithMany();
carEtb.HasIndex(c => c.DriverID).IsUnique();

driverEtb.HasOne(d => d.Car).WithMany();
driverEtb.HasIndex(c => c.CarID).IsUnique();
Run Code Online (Sandbox Code Playgroud)

它创建两个可空的外键,它们都由一个唯一的过滤索引索引。完美的!

但...

EF 不认为这是双向的一对一关系。理所当然。两个 FK 就是这样,两个独立的 FK。然而,从数据完整性的角度来看,这种关系应该由两端建立:如果一个司机声称有一辆车(sets driver.CarID),那么这辆车也应该附加到这个司机(set car.DriverID)上,否则另一个司机可能会连接到它。

当现有的汽车和司机耦合时,可以使用一个小助手方法,例如Car

  CREATE TABLE [Drivers] (
      [ID] int NOT NULL IDENTITY,
      [Name] nvarchar(max) NULL,
      [CarID] int NULL,
      CONSTRAINT [PK_Drivers] PRIMARY KEY ([ID])
  );

  CREATE TABLE [Cars] (
      [ID] int NOT NULL IDENTITY,
      [Brand] nvarchar(max) NULL,
      [Type] nvarchar(max) NULL,
      [DriverID] int NULL,
      CONSTRAINT [PK_Cars] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_Cars_Drivers_DriverID] FOREIGN KEY ([DriverID])
          REFERENCES [Drivers] ([ID]) ON DELETE NO ACTION
  );

  CREATE UNIQUE INDEX [IX_Cars_DriverID] ON [Cars] ([DriverID])
      WHERE [DriverID] IS NOT NULL;


  CREATE UNIQUE INDEX [IX_Drivers_CarID] ON [Drivers] ([CarID])
      WHERE [CarID] IS NOT NULL;

  ALTER TABLE [Drivers] ADD CONSTRAINT [FK_Drivers_Cars_CarID] FOREIGN KEY ([CarID])
      REFERENCES [Cars] ([ID]) ON DELETE NO ACTION;
Run Code Online (Sandbox Code Playgroud)

但是,当 aCarDriver都在一个进程中创建关联时,这是很笨拙的。EF 会抛出一个InvalidOperationException

无法保存更改,因为在要保存的数据中检测到循环依赖关系:'Car [Added] <- Car { 'CarID' } Driver [Added] <- Driver { 'DriverID' } Car [Added]'。

意思是:其中一个FK可以一次性设置,而另一个只能在保存数据后设置。这需要SaveChanges在一段非常命令式的代码中包含在事务中的两个调用:

public void SetDriver(Driver driver)
{
    Driver = driver;
    driver.Car = this;
}
Run Code Online (Sandbox Code Playgroud)

替代方案:连接表

所以现在我之所以用这些长度来解释所有这些:在我看来,0..1 : 0..1关联应该由具有唯一外键的联结表建模:

OntToOneJunction

通过使用连接表 -

  1. 可以在原子操作中建立关联,而不是设置两个外键的容易出错的操作。
  2. 实体本身是独立的:它们没有外键,它们实际上不需要履行其职责。

这个模型可以通过这个类模型来实现:

using (var db = new MyContext())
{
    using (var t = db.Database.BeginTransaction())
    {
        var jag = new Car { Brand = "Jaguar", Type = "E" };
        var peter = new Driver { Name = "Peter Sellers", Car = jag };

        db.Drivers.Add(peter);

        db.SaveChanges();

        jag.Driver = peter;

        db.SaveChanges();
        t.Commit();
    }
}
Run Code Online (Sandbox Code Playgroud)

和映射:

public class Car
{
    public int ID { get; set; }
    public string Brand { get; set; }
    public string Type { get; set; }
    public CarDriver CarDriver { get; set; }
}

public class Driver
{
    public Driver()
    { }
    public int ID { get; set; }
    public string Name { get; set; }
    public CarDriver CarDriver { get; set; }
}

public class CarDriver
{
    public int CarID { get; set; }
    public Car Car { get; set; }
    public int DriverID { get; set; }
    public virtual Driver Driver { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

现在创建车手和赛车他们的协会可以很容易地在一个做SaveChanges呼叫:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var carDriverEtb = modelBuilder.Entity<CarDriver>();
    carDriverEtb.HasKey(cd => new { cd.CarID, cd.DriverID });
    carDriverEtb.HasIndex(cd => cd.CarID).IsUnique();
    carDriverEtb.HasIndex(cd => cd.DriverID).IsUnique();
}
Run Code Online (Sandbox Code Playgroud)

唯一的缺点是,从navigtingCarDriverVV是有点莱方便。好吧,您自己看看哪种型号最适合您。