是否可以在实体框架中捕获 0..1 到 0..1 的关系? [英] Is it possible to capture a 0..1 to 0..1 relationship in Entity Framework?

查看:30
本文介绍了是否可以在实体框架中捕获 0..1 到 0..1 的关系?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

<块引用>

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

public class Type1 {公共 int ID { 获取;放;}公众号?Type2ID { 获取;放;}公共类型 2 类型 2 { 获取;放;}}公共类 Type2 {公共 int ID { 获取;放;}公众号?Type1ID { 获取;放;}公共类型 1 类型 1 { 获取;放;}}

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

Type1 Type2======================身份证件|类型1ID————————————1 1 |空值2 2 |2

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

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

有一个有趣的解决方法提出了替代映射.我试过了,但有两个可选的关联:

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,它有两个外键,分别为CarDriver,这两个外键都包含其唯一的主键:

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

公共类汽车{公共 int CarId { 获取;放;}公共字符串名称 { 获取;放;}公共虚拟 ICollection<Driver>司机{得到;放;}}公开课司机{公共 int DriverId { 获取;放;}公共字符串名称 { 获取;放;}公共虚拟 ICollection<Car>汽车 { 得到;放;}}

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

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

选项 4

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

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

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

但是,由于 EF6 默认在 FK 字段上添加索引,唯一索引添加在默认的非唯一索引之上.所以还是需要在迁移代码中进行人工干预才能去除后者.

结论

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

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

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

Is there a way to make a nullable reverse navigation property for a nullable Foreign Key relationship in Entity Framework? In database parlance, a 0..1 to 0..1 relationship.

I've tried as below, but I keep getting the error message:

Unable to determine the principal end of an association between the types 'Type1' and 'Type2'. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

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

I understand that the integer column can only exist in one table or the other, but surely it should be possible to cover all necessary cases? E.g:

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

I've tried using data annotations (e.g. [ForeignKey] on one end, [InverseProperty] on both), but none of those seem to help the matter.

If possible, a data annotation solution would be preferred over Fluent API. Also, the int? property isn't strictly necessary from a domain perspective for either class, if that helps.

There is an interesting work-around here which implies it isn't possible to capture this kind of relationship in Entity Framework (effectively, an item that is optionally part of a collection) - if so, is there any documentation that would support this?.

解决方案

In EF6 and earlier it wasn't all that easy to implement such an association correctly. Fortunately, EF-core has greatly improved in supported associations. Now it's a piece of cake to implement the only model that enforces this kind of association by database constraints. That is: a junction class between Car and Driver in which the foreign keys have unique indexes (option 4 below). And it even almost entirely works with default mapping conventions.

The model:

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

The only required explicit mapping:

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

That's all EF needs to create the correct database model:

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]);

These two indexes at the end are the icing on the piece of cake. They show that EF exactly understands what's going on here.


Original, but updated, answer

"This can't be hard" is what I though when I read your question. But again I found that one-to-one associations are full of pitfalls. Here we go.

I assume that by 0..1 – 0..1 you mean that two objects can exist independent of each other, but may also be exclusively associated to one another.

Lets make it concrete. Car and Driver. Imagine a pool of many cars and drivers, among them CarA and a DriverA. Now suppose you want CarA to get associated to DriverA, and your implementation is that DriverA links himself to CarA. But as soon as DriverA does this, you want CarA to be for DriverA only, CarA's association is not optional any more, so it should be set as well, immediately.

How to implement that?

Option 1:

If this is the working model:

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

technically, DriverA can have a foreign key to CarA and CarA a foreign key to DriverB.

Therefore, when the foreign key DriverA-CarA is established you should "simulaneously" establish the reverse foreign key CarA-DriverA. That is something you should do in code, meaning that it's a business rule. And in reality, it's not an atomic operation, so you must make sure that it's done in one database transaction.

The class model at least supports the use case, but it's too permissive. It needs to be constrained. More importantly, it won't work with EF. EF complaints about having to set a principal end. And if you do that, EF will not create a bidirectional association.

An alternative mapping was proposed here. I tried that but with two optional associations:

In the Driver's mapping configuration:

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

In the Car's mapping configuration:

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

(There is no data annotation alternative)

I found that EF only sets one foreign key value in the database when creating a new driver and car. You have to set and save both associations separately, managing your own transaction. With existing objects you still have to set both foreign keys, although this can be saved in one SaveChanges call.

Better options? Let's see...

Option 2:

This is the one-to-many association as mentioned in the link you refer to. This model needs external constraints, but creating the association is atomic. And you've still got a reference on one end and a collection on the other end. And it maps easily with EF.

Option 3:

You could create a junction table CarDriver that has two foreign keys, to Car and Driver, both of which comprise its unique primary key:

This is a regular many-to-many association. By default, EF would map this as a class model in which Car and Driver have collection properties pointing to each other, and the junction table is not mapped directly:

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

Now the creation of association is an atomic operation. It's perfectly possible to map this model with EF. The mutual references are gone, but you still can get the FirstOrDefault() of the collection properties as a surrogate reference.

But there's an important gotcha. Now each object can have any number of associated counterparts. If you create an association, you need a coded business rule which checks if the the involved objects don't have any associations yet. Maybe this option is even worse than option 2. But I mentioned it because of the next option:

Option 4

Option 3 is atomic, but it also needs external constraints. To make an association exclusive, both columns in CarDriver should have unique keys, so each car or driver can only occur once in the table. By these indexes the model implements a bidirectionally optional 1:1 association all by itself. Any code working on it has to obey the rules. Safe and sound...

In EF6, since the introduction of HasIndex, this can be achieved by this mapping:

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

However, since EF6 adds indexes on FK fields by default, the unique indexes are added on top of the default non-unique indexes. So it still requires manual intervention in the migration code to remove the latter.

Conclusions

Option 1 is closest to what you want. But I don't like the obligation to set both foreign keys, it's easily forgotten or ignored by future developers.

But Option 2 and 3 have even heavier requirements in terms of coded business rules that can be forgotten. And the collections are unnatural as surrogate "1" ends. Option 3 has some appeal to me because Car and Driver are completely independent in the database and the association is a record with non-nullable foreign keys (DBAs tend to like that too).

Option 4 has the same appeal, and it's the best option when multiple applications would have to implement the external constraints that need to be imposed on option 2 and 3. Also, even if coded rules are forgotten, the database constraints are a final catch. But it can't easily be implemented by EF6.

这篇关于是否可以在实体框架中捕获 0..1 到 0..1 的关系?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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