Entity Framework Core 零或一到零或一关系 [英] Entity Framework Core zero-or-one to zero-or-one relation

查看:19
本文介绍了Entity Framework Core 零或一到零或一关系的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给定这些类:

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;} 
}

然后使用 Fluent API

Then with Fluent API

 modelBuilder.Entity<A>()
                .HasOne(a => a.B)
                .WithOne(b => b.A)
                .HasForeignKey<A>(a => a.BId);

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

When creating objects and add them to database, things look like following in the corresponding tables:

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

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

When I retrieve data using EF Core:

  • A.B 已设置,A.Bid 已设置
  • B.A 已设置,但 B.AId 为空.

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

What should I do to have B.AId set as well?

推荐答案

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

These 0..1 : 0..1 relations are usually defined between entities of which none is an obvious principal entity. I like the example of cars and drivers, which is a bit more imaginable than A and B.

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

The model you're after looks like this:

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

There are two mutual foreign keys, both of which have a unique index to enforce 1:1 at the database level.

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

The HasOne - WithOne combi can't be used here, because that always requires a HasForeignKey instruction to tell which entity is principal. This also configures only one field as foreign key. In your example, B.AId is just a regular field. If you don't give it a value, EF won't either.

上述模型的映射比HasOne - WithOne要麻烦一些:

The mapping of the above model is a bit more cumbersome than 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();

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

So there are two 0..1:n association that are made unique by indexes on the foreign keys.

创建以下数据库模型:

  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;

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

It creates two nullable foreign keys both indexed by a unique filtered index. Perfect!

可是……

EF 不认为这是双向的一对一关系.理所当然.两个 FK 就是这样,两个独立的 FK.但是,从数据完整性的角度来看,这种关系应该由两端建立:如果司机认领汽车(设置driver.CarID),汽车也应该附加到司机(设置car.DriverID),否则其他驱动程序可能会连接到它.

EF doesn't see this as a bidirectional one-on-one relationship. And rightly so. The two FKs are just that, two independent FKs. However, in view of data integrity the relationship should be established by both ends: if a driver claims a car (sets driver.CarID), the car should also be attached to the driver (set car.DriverID), otherwise another driver could be connected to it.

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

When existing car and drivers are coupled a little helper method could be used, for example in Car:

public void SetDriver(Driver driver)
{
    Driver = driver;
    driver.Car = this;
}

然而,当 CarDriver 在一个进程中被创建和关联时,这是很笨拙的.EF 会抛出一个 InvalidOperationException:

However, when both a Car and Driver are created and associated in one process, this is clumsy. EF will throw an InvalidOperationException:

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

Unable to save changes because a circular dependency was detected in the data to be saved: 'Car [Added] <- Car { 'CarID' } Driver [Added] <- Driver { 'DriverID' } Car [Added]'.

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

Which means: one of the FKs can be be set at once, but the other one can only be set after saving the data. That requires two SaveChanges calls enclosed by a transaction in a pretty imperative piece of code:

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();
    }
}

替代方案:连接表

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

Alternative: junction table

So now the reason why I go to these lengths explaining all this: in my opinion, 0..1 : 0..1 associations should be modeled by a junction table with unique foreign keys:

通过使用联结表 -

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

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

This model can be implemented by this class model:

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; }
}

和映射:

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();
}

现在可以通过一个 SaveChanges 调用轻松创建驾驶员和汽车以及它们的关联:

Now creating drivers and cars and their associations can easily be done in one SaveChanges call:

using (var db = new MyContext(connectionString))
{
    var ford = new Car { Brand = "Ford", Type = "Mustang" };
    var jag = new Car { Brand = "Jaguar", Type = "E" };

    var kelly = new Driver { Name = "Kelly Clarkson" };
    var peter = new Driver { Name = "Peter Sellers" };

    db.CarDrivers.Add(new CarDriver { Car = ford, Driver = kelly });
    db.CarDrivers.Add(new CarDriver { Car = jag, Driver = peter });

    db.SaveChanges();
}

唯一的缺点是从 Car 导航到 Driver vv 有点不方便.好吧,您自己看看哪种型号最适合您.

The only drawback is that navigting from Car to Driver vv is a bit les convenient. Well, see for yourself which model suit you best.

这篇关于Entity Framework Core 零或一到零或一关系的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆