C# 数据操作系列 - 7. EF Core 导航属性配置

在上一篇,大概介绍了Entity Framework Core关于关系映射的逻辑。在上一篇中留下了EF的外键映射没有说,也就是一对一,一对多,多对一,多对多的关系等。这一篇将为大家细细分析一下,如何设置这些映射。

1. 实体之间的关系

从数据表来考虑,两个表之前的关系有一对一,一对多(多对一)和多对多的关系。

其中一对一,指的是表A有一条记录对应着表B最多有一条记录与之对应。反过来也一样,表A也最多有一条记录与表B的某一条记录对应。具体在数据表上表现为,A表和B表各有一个外键指向对方。

一对多和多对一是一个概念,只是参考的方向是相反的。所谓的一对多就是其中多方上有一个属性或者列指向了另一个实体,而那个“一”的那头则没有对应的属性指向多方。

多对多是指两个类的实例各有一个集合属性指向对方,换句话说就是A有0到多个B,B也有0到多个A。这里有一个关于多对多的ER图。

image-20200515220140873

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/fwlink/?LinkId=724062 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 在开发中的用法。

更多内容烦请关注我的博客《高先生小屋》

file
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355