在上一篇,大概介绍了 Entity Framework Core 关于关系映射的逻辑。在上一篇中留下了 EF 的外键映射没有说,也就是一对一,一对多,多对一,多对多的关系等。这一篇将为大家细细分析一下,如何设置这些映射。
1. 实体之间的关系
从数据表来考虑,两个表之前的关系有一对一,一对多(多对一)和多对多的关系。
其中一对一,指的是表 A 有一条记录对应着表 B 最多有一条记录与之对应。反过来也一样,表 A 也最多有一条记录与表 B 的某一条记录对应。具体在数据表上表现为,A 表和 B 表各有一个外键指向对方。
一对多和多对一是一个概念,只是参考的方向是相反的。所谓的一对多就是其中多方上有一个属性或者列指向了另一个实体,而那个“一”的那头则没有对应的属性指向多方。
多对多是指两个类的实例各有一个集合属性指向对方,换句话说就是 A 有 0 到多个 B,B 也有 0 到多个 A。这里有一个关于多对多的 ER 图。
2. 一对一关系
先给出两个示例类,为了方便理解,我只保留了主键和导航属性:
public class SingleModel
{public int Id { get; set;}
public SingleTargetModel SingleTarget {get; set;}
}
public class SingleTargetModel
{public int Id { get; set;}
public SingleModel Single {get; set;}
}
那么我们开始写配置文件:
public class SingleModelConfig : IEntityTypeConfiguration<SingleModel>
{public void Configure(EntityTypeBuilder<SingleModel> builder)
{builder.ToTable("SingleModel");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
var relation = builder.HasOne(t => t.SingleTarget).WithOne(r => r.Single);
}
}
public class SingleTargeModelConfig : IEntityTypeConfiguration<SingleTargetModel>
{public void Configure(EntityTypeBuilder<SingleTargetModel> builder)
{builder.ToTable("SingleTargetModel");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();}
}
其中 HasOne 表示当前实体是关系中“一”,WithOne 表示导航目标类的关系。
当然,如果直接应用这两个配置到 EF Context 的话,在执行
Update-Database
会报以下错误:
The child/dependent side could not be determined for the one-to-one relationship between ‘SingleModel.SingleTarget’ and ‘SingleTargetModel.Single’. To identify the child/dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlin… for more details.
意思就是无法定义一对一关系中的子 / 从属方
如何解决呢?之前在说的时候,EF 会根据导航属性自动生成一个外键,但是这一条在一对一这里就有点不太起作用了。所以我们必须手动在导航属性的一侧实体类里配置外键,并用 HasForeignKey 指定。(如果不使用 Fluent API,也是需要在一端实体类配置外键,另一端则不需要)。
修改后:
public class SingleModel
{public int Id { get; set;}
public int TargetId {get; set;}
public SingleTargetModel SingleTarget {get; set;}
}
public class SingleTargetModel
{public int Id { get; set;}
public SingleModel Single {get; set;}
}
所以最终的配置应该如下:
public class SingleModelConfig : IEntityTypeConfiguration<SingleModel>
{public void Configure(EntityTypeBuilder<SingleModel> builder)
{builder.ToTable("SingleModel");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
builder.HasOne(t => t.SingleTarget).WithOne(r => r.Single).HasForeignKey<SingleModel>(t=>t.TargetId);
}
}
public class SingleTargeModelConfig : IEntityTypeConfiguration<SingleTargetModel>
{public void Configure(EntityTypeBuilder<SingleTargetModel> builder)
{builder.ToTable("SingleTargetModel");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
//builder.HasOne(t => t.Single).WithOne(r => r.SingleTarget).HasForeignKey<SingleTargetModel>("SingleId");
}
}
注意我注释的这一行,现在 EF 只在 SingleModel 表中生成了一个外键关系,在检索 SingleTargetModel 的时候,EF 会从 SingleModel 表中检索对应的外键关系,并引入进来。
如果取消这行注释,EF 会在 SingleTargetModel 表添加一个名为 SingleId 并指向 SingleModel 的外键,而取消 SingleModel 里的外键。
但是 ,这时候如果在 SingleTargetModel 里添加了一个非空属性的 SingleId,SQLite 插入数据时会报错。错误信息:
SQLite Error 19: ‘FOREIGN KEY constraint failed’.
其他数据库提示,外键不能为空。
所以也就是说 EF 不推荐这种双方互导航的一对一关系。
这是生成的 DDL SQL 语句:
create table SingleModel
(
Id INTEGER not null
constraint PK_SingleModel
primary key autoincrement,
TargetId INTEGER not null
constraint FK_SingleModel_SingleTargetModel_TargetId
references SingleTargetModel
on delete cascade
);
create unique index IX_SingleModel_TargetId
on SingleModel (TargetId);
create table SingleTargetModel
(
Id INTEGER not null
constraint PK_SingleTargetModel
primary key autoincrement
);
3. 一对多或多对一
照例,先来两个类:
public class OneToManySingle
{public int Id { get; set;}
public List<OneToManyMany> Manies {get; set;}
}
public class OneToManyMany
{public int Id { get; set;}
public OneToManySingle One {get; set;}
}
如果从 OneToManySingle 来看,这个关系是一对多,如果从 OneToManyMany 来看的话这个关系就是多对一。
那么我们看一下一对多的配置吧:
public class OneToManySingleConfig : IEntityTypeConfiguration<OneToManySingle>
{public void Configure(EntityTypeBuilder<OneToManySingle> builder)
{builder.ToTable("OneToManySingle");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
builder.HasMany(t => t.Manies)
.WithOne(p => p.One);
}
}
public class OneToManyManyConfig : IEntityTypeConfiguration<OneToManyMany>
{public void Configure(EntityTypeBuilder<OneToManyMany> builder)
{builder.ToTable("OneToManyMany");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
//builder.HasOne(p => p.One).WithMany(t=>t.Manies);
}
}
在使用隐式外键的时候,只需要设置导航属性的关联即可。如果想在 Single 端设置,需要先用 HasMany 表示要设置一个多对 X 的关系,然后调用 WithOne 表示是多对一。如果是 Many 端,则必须先声明是 HasOne。
其中 WithXXX 里的参数可以省略,如果只是配置了单向导航的话。
如果显示声明了外键,需要用 HasForeignKey 来标注外键。
以下是生成的 DDL SQL 语句:
create table OneToManySingle
(
Id INTEGER not null
constraint PK_OneToManySingle
primary key autoincrement
);
create table OneToManyMany
(
Id INTEGER not null
constraint PK_OneToManyMany
primary key autoincrement,
OneId INTEGER
constraint FK_OneToManyMany_OneToManySingle_OneId
references OneToManySingle
on delete restrict
);
create index IX_OneToManyMany_OneId
on OneToManyMany (OneId);
4. 多对多
在讲多对多的时候,需要先明白一个概念。多对多,对于导航两端来说,是无法在自己身上找到对应的标记的。也就是说,各自的数据表不会出现指向对方的外键。那么,如何实现多对多呢?增加一个专门的中间表,用来存放两者之间的关系。
EF Core 中取消了在映射关系中配置中间表的功能,所以在 EF Core 中需要一个中间表:
public class ManyToManyModelA
{public int Id { get; set;}
public List<ModelAToModelB> ModelBs {get; set;}
}
public class ModelAToModelB
{public int Id { get; set;}
public ManyToManyModelA ModelA {get; set;}
public ManyToManyModelB ModelB {get; set;}
}
public class ManyToManyModelB
{public int Id { get; set;}
public List<ModelAToModelB> ModelAs {get; set;}
}
那么继续看一下配置文件:
public class ManyToManyToModelAConfig : IEntityTypeConfiguration<ManyToManyModelA>
{public void Configure(EntityTypeBuilder<ManyToManyModelA> builder)
{builder.ToTable("ManyToManyModelA");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
builder.HasMany(t => t.ModelBs).WithOne(p => p.ModelA);
}
}
public class ManyToManyModelBConfig : IEntityTypeConfiguration<ManyToManyModelB>
{public void Configure(EntityTypeBuilder<ManyToManyModelB> builder)
{builder.ToTable("ManyToManyModelB");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedOnAdd();
builder.HasMany(t => t.ModelAs).WithOne(p => p.ModelB);
}
}
与一对多的关系不同的地方是,这个需要两方都配置一个多对一的映射,指向中间表。
在 EF 6 中 中间表可以仅存在于关系中,但是在 EF Core3 还没有这个的支持。也就是当前文章使用的版本。
5. 附加
在 EF 的外键约束中,导航属性是默认可空的。如果要求非空,也就是导航属性的另一端必须存在则需要在配置关系的时候添加:
IsRequired()
这个方法也用来声明字段是必须的。这个验证是在 EF 调用 SaveChanges 的时候校验的。
6. 未完待续
照例的未完待续,下一篇将为大家介绍一下 EF Core 在开发中的用法。
更多内容烦请关注我的博客《高先生小屋》