Entity Framework 核心零或一到零或一的关系
Entity Framework Core zero-or-one to zero-or-one relation
鉴于这些 类:
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
modelBuilder.Entity<A>()
.HasOne(a => a.B)
.WithOne(b => b.A)
.HasForeignKey<A>(a => a.BId);
创建对象并将它们添加到数据库时,对应表中的内容如下所示:
- [A].BId 已设置
- [B].AId = null
当我使用 EF Core 检索数据时:
- A.B已设置,A.BId已设置
- B.A 已设置,但 B.AId 为空。
我应该怎么做才能同时设置 B.AId?
这些0..1 : 0..1
关系通常定义在实体之间,none是一个明显的主要实体。我喜欢汽车和drivers的例子,比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();
因此有两个 0..1:n 关联由外键上的索引制成唯一。
创建以下数据库模型:
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;
它创建两个可为空的外键,它们都由唯一的 filtered 索引索引。完美!
但是...
EF 不认为这是双向 one-on-one 关系。确实如此。这两个 FK 就是两个独立的 FK。然而,考虑到数据完整性,这种关系应该由两端建立:如果 driver 声明了一辆汽车(设置 driver.CarID
),那么汽车也应该附加到 driver(设置car.DriverID
), 否则另一个 driver 可以连接到它。
当现有汽车和 driver 耦合时,可以使用一些辅助方法,例如 Car
:
public void SetDriver(Driver driver)
{
Driver = driver;
driver.Car = this;
}
但是,当 Car
和 Driver
都在一个进程中创建 和 时,这是笨拙的。 EF 将抛出 InvalidOperationException
:
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
调用包含在一个非常必要的代码片段中的事务中:
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();
}
}
备选方案:交界处table
所以现在我为什么要花这么多时间解释这一切的原因:在我看来,0..1 : 0..1
关联应该由具有唯一外键的联结 table 建模:
通过使用结点 table -
- 可以在原子操作中建立关联,而不是 error-prone 设置两个外键的操作。
- 实体本身是独立的:它们没有外键,它们实际上并不需要完成它们的任务。
这个模型可以用这个class模型实现:
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();
}
现在创建 driver 和汽车 和 它们的关联可以在一次 SaveChanges
调用中轻松完成:
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 有点不方便。好吧,你自己看看哪个型号最适合你。
在 EFCore 3.1 中,您可以像这样建立一对零关系:
public class Car
{
public int Id { get; set; }
public int DriverId { get; set; }
public Driver Driver { get; set; }
}
public class Driver
{
public int Id { get; set; }
public Car Driver { get; set; }
}
在您的 dbContext 中
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasOne(x => x.Driver)
.WithOne(x => x.Car)
.HasForeignKey<Car>(x => x.DriverId);
}
那么你可以包括:
driver 从汽车
dbContext.Driver.Inclide(x => x.Car)...
driver
的车
dbContext.Car.Inclide(x => x.Driver)...
鉴于这些 类:
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
modelBuilder.Entity<A>()
.HasOne(a => a.B)
.WithOne(b => b.A)
.HasForeignKey<A>(a => a.BId);
创建对象并将它们添加到数据库时,对应表中的内容如下所示:
- [A].BId 已设置
- [B].AId = null
当我使用 EF Core 检索数据时:
- A.B已设置,A.BId已设置
- B.A 已设置,但 B.AId 为空。
我应该怎么做才能同时设置 B.AId?
这些0..1 : 0..1
关系通常定义在实体之间,none是一个明显的主要实体。我喜欢汽车和drivers的例子,比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();
因此有两个 0..1:n 关联由外键上的索引制成唯一。
创建以下数据库模型:
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;
它创建两个可为空的外键,它们都由唯一的 filtered 索引索引。完美!
但是...
EF 不认为这是双向 one-on-one 关系。确实如此。这两个 FK 就是两个独立的 FK。然而,考虑到数据完整性,这种关系应该由两端建立:如果 driver 声明了一辆汽车(设置 driver.CarID
),那么汽车也应该附加到 driver(设置car.DriverID
), 否则另一个 driver 可以连接到它。
当现有汽车和 driver 耦合时,可以使用一些辅助方法,例如 Car
:
public void SetDriver(Driver driver)
{
Driver = driver;
driver.Car = this;
}
但是,当 Car
和 Driver
都在一个进程中创建 和 时,这是笨拙的。 EF 将抛出 InvalidOperationException
:
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
调用包含在一个非常必要的代码片段中的事务中:
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();
}
}
备选方案:交界处table
所以现在我为什么要花这么多时间解释这一切的原因:在我看来,0..1 : 0..1
关联应该由具有唯一外键的联结 table 建模:
通过使用结点 table -
- 可以在原子操作中建立关联,而不是 error-prone 设置两个外键的操作。
- 实体本身是独立的:它们没有外键,它们实际上并不需要完成它们的任务。
这个模型可以用这个class模型实现:
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();
}
现在创建 driver 和汽车 和 它们的关联可以在一次 SaveChanges
调用中轻松完成:
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 有点不方便。好吧,你自己看看哪个型号最适合你。
在 EFCore 3.1 中,您可以像这样建立一对零关系:
public class Car
{
public int Id { get; set; }
public int DriverId { get; set; }
public Driver Driver { get; set; }
}
public class Driver
{
public int Id { get; set; }
public Car Driver { get; set; }
}
在您的 dbContext 中
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasOne(x => x.Driver)
.WithOne(x => x.Car)
.HasForeignKey<Car>(x => x.DriverId);
}
那么你可以包括:
driver 从汽车
dbContext.Driver.Inclide(x => x.Car)...
driver
的车dbContext.Car.Inclide(x => x.Driver)...