在 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不容易实现。