假设有以下两个实体:

public class Student
{
public int StuID { get; set; }
public string? Name { get; set; }
public IEnumerable<Homework>? Homeworks { get; set; }
} public class Homework
{
public string? Class { get; set; }
public string? Subject { get; set; }
}

Homework 类表示家庭作业,它并不是独立使用的,而是与学生类(Student)有依赖关系。一位学生有多个家庭作业记录,即 Homework 对象用于记录每位同学的作业的。按照这样的前提,Student 是主对象,Homework 是从对象。

Student 对象有个 Homeworks 属性,用于引用 Homework 对象,也就是所谓的“导航属性”。这个“导航”,估计意思就是你通过这个属性可以找到被引用的另一个实体对象,所以称之为导航,就是从 Navigation 的翻译。

随后,咱们要从 DbContext 类派生出自定义的数据库上下文。

public class MyDbContext : DbContext
{
// 映射的数据表,名称默认与属性名称一样
// 即 Students + Works
public DbSet<Student> Students => Set<Student>();
public DbSet<Homework> Works => Set<Homework>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 设置连接字符串
optionsBuilder.UseSqlServer(Helper.Conn_STRING);
} protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 设置主键
modelBuilder.Entity<Student>().HasKey(s => s.StuID);
// 建立主从关系
modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
}
}

连接字符串是老周事先配置好的,连的是 SQL Server。

public class Helper
{
public const string Conn_STRING = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True";
}

用的是 LocalDB,这玩意儿方便。

其实这是一个控制台应用程序,并添加了 Nuget 包。

  <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
</ItemGroup>

好,回到咱们的代码中,MyDbContext 重写了两个方法:

1、重写 OnConfiguring 方法,做一些与该 Context 有关的配置,通常是配置连接字符串;也可能配置一下日志输出。上面代码中使用的是扩展方法 UseSqlServer。这就是引用 Microsoft.EntityFrameworkCore.SqlServer Nuget 包的作用。

2、重写 OnModelCreating 方法。这个是设置实体类相关的模型属性,以及与数据表的映射,或配置实体之间的关系。上述代码中,老周做了两件事:A、为 Student 实体设置主键,作为主键的属性是 StuID;B、建立 Student 和 Homework 对象的主从关系,调用 OwnsMany 方法的意思是:一条 Student 记录对应 N 条 Homework 记录。因为 Student 类的 Homeworks 属性是集合。

注意:咱们此处是先建了实体类,运行后才创建数据库的,所以不需要生成迁移代码。

在 Main 方法中,咱们要做两件事:A、根据上面的建模创建数据库;B、往数据库中存一点数据。

static void Main(string[] args)
{
using (var ctx = new MyDbContext())
{
//ctx.Database.EnsureDeleted();
bool res = ctx.Database.EnsureCreated();
if (res)
{
Console.WriteLine("已创建数据库");
}
} using(MyDbContext ctx = new())
{
// 加点料
ctx.Students.Add(new Student
{
Name = "小张",
Homeworks = new List<Homework>
{
new Homework{ Class = "数学", Subject = "3000道口算题"},
new Homework{ Class = "英语", Subject = "背9999个单词"}
}
}); ctx.Students.Add(new Student
{
Name = "小雪",
Homeworks = new Homework[]
{
new Homework{ Class = "历史", Subject = "临一幅《清明上河图》"},
new Homework{ Class = "语文", Subject = "作文题:《百鬼日行》"}
}
}); // 保存
int x = ctx.SaveChanges();
Console.WriteLine("共保存了{0}条记录", x);
}
}

EnsureCreated 方法会自动创建数据库。如果不存在数据库且创建成功,返回 true,否则是 false。数据库的名称在连接字符串中配置过。

Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True

接下来,我们运行一下。稍等几秒钟,看到控制台输出下面文本就算成功了。

已创建数据库
共保存了6条记录

然后,连上去看看有没有数据库。

看看,这表的名称是不是和 MyDbContext 的两个属性一样?

public class MyDbContext : DbContext
{
public DbSet<Student> Students => Set<Student>();
public DbSet<Homework> Works => Set<Homework>();
……

你要是不喜欢用这俩名字,也可以发动传统技能(指老 EF),用 Table 特性给它们另取高名。

[Table("tb_students", Schema = "dbo")]
public class Student
{
……
} [Table("tb_homeworks", Schema = "dbo")]
public class Homework
{
……
}

删除数据库,再运行一次程序,然后再登录数据库看看,表名变了吗?

那有伙伴们会问:有没有现代技能?有的,使用 ToTable 方法定义映射的数据表名称。

先去掉 Student、Homework 类上的 Table 特性,然后直接在重写 OnModelCreating 方法时配置。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().ToTable("dt_students").HasKey(s => s.StuID);
modelBuilder.Entity<Homework>().ToTable("dt_works");
// 建立主从关系
modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
}

但是这样写会报错的。因为 Homework 实体是 Student 的从属对象,单独调用 ToTable 方法在配置的时候会将其设置为独立对象,而非从属对象。

所以,正确的做法是在两个实体建立了从属性关系后再调用 ToTable 方法(Student 对象是主对象,它可以单独调用)。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().HasKey(s => s.StuID);
modelBuilder.Entity<Student>()
.ToTable("tb_students")
.OwnsMany(s => s.Homeworks)
.ToTable("tb_works");
}

因为 Homework 是 Student 的从属,tb_works 表中要存在一个外键——引用 Student.StuID,这样两个表才能建立主从关系。如果单独调用 Entity<Homework>.ToTable 映射表的话,那么表中不会添加引用 StuID 的外键列。就是默认被配置为非主从模式。没有了外键,tb_works 表中存的数据就无法知道是哪位学生的作业了。

这样创建数据库后,tb_works 表中就存在名为 StudentStuID 的列,它就是引用 Student.StuID 的外键。

CREATE TABLE [dbo].[tb_works] (
[StudentStuID] INT NOT NULL,
[Id] INT IDENTITY (1, 1) NOT NULL,
[Class] NVARCHAR (MAX) NULL,
[Subject] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([StudentStuID] ASC, [Id] ASC),
CONSTRAINT [FK_tb_works_tb_students_StudentStuID] FOREIGN KEY ([StudentStuID]) REFERENCES [dbo].[tb_students] ([StuID]) ON DELETE CASCADE
);

当然,这个外键名字是根据实体类名(Student)和它的主键属性名(StuID)生成的,如果你想自己搞个名字,也是可以的。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().HasKey(s => s.StuID);
modelBuilder.Entity<Student>()
.ToTable("tb_students")
.OwnsMany(s => s.Homeworks, tb =>
{
tb.ToTable("tb_works");
tb.WithOwner().HasForeignKey("student_id");
});
}

这样 tb_works 表中就有了名为 student_id 的外键。

CREATE TABLE [dbo].[tb_works] (
[student_id] INT NOT NULL,
[Id] INT IDENTITY (1, 1) NOT NULL,
[Class] NVARCHAR (MAX) NULL,
[Subject] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([student_id] ASC, [Id] ASC),
CONSTRAINT [FK_tb_works_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([StuID]) ON DELETE CASCADE
);

OwnsXXX 方法是指:俺是主表,我要“关照”一下从表;

WithOwner 方法是指:俺是从表,我要配置一下和主表之间建立联系的参数(如上面给外键另起个名字)。

那么,我想把两个表的列全自定义命名,可以吗?当然可以的。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>().HasKey(s => s.StuID);
modelBuilder.Entity<Student>()
.ToTable("tb_students", tb =>
{
tb.Property(s => s.StuID).HasColumnName("sID");
tb.Property(s => s.Name).HasColumnName("stu_name");
})
.OwnsMany(s => s.Homeworks, tb =>
{
tb.ToTable("tb_works");
tb.WithOwner().HasForeignKey("student_id");
tb.Property(w => w.Class).HasColumnName("wk_class");
tb.Property(w => w.Subject).HasColumnName("wk_sub");
});
}

两个表的字段名都变了。

CREATE TABLE [dbo].[tb_students] (
[sID] INT IDENTITY (1, 1) NOT NULL,
[stu_name] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_tb_students] PRIMARY KEY CLUSTERED ([sID] ASC)
); CREATE TABLE [dbo].[tb_works] (
[student_id] INT NOT NULL,
[Id] INT IDENTITY (1, 1) NOT NULL,
[wk_class] NVARCHAR (MAX) NULL,
[wk_sub] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_tb_works] PRIMARY KEY CLUSTERED ([student_id] ASC, [Id] ASC),
CONSTRAINT [FK_tb_works_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([sID]) ON DELETE CASCADE
);

注意:Homework 类中没有定义 Id 属性(主键),它是自动生成的。

有大伙伴会想,在 OnModelCreating 方法中建模我头有点晕,我能不能在定义实体类的时候,直接通过特性批注来实现主从关系呢?那肯定可以的了。

[Table("tb_students")]
[PrimaryKey(nameof(StuID))]
public class Student
{
[Column("sID")]
public int StuID { get; set; } [Column("st_name")]
public string? Name { get; set; } // 这是导航属性,不需要映射到数据表
public IEnumerable<Homework>? Homeworks { get; set; }
} [Owned]
[Table("tb_homeworks")]
[PrimaryKey(nameof(wID))]
public class Homework
{
[Column("wk_id")]
public int wID { get; set; } [Column("wk_class")]
public string? Class { get; set; } [Column("wk_sub")]
public string? Subject { get; set; } [ForeignKey("student_id")] //设置外键名称
public Student? StudentObj { get; set; }
}

PrimaryKey 特性设置实体类中哪些属性为主键,使用属性成员的名称,而不是数据表字段名称。

在 Homework 类上用到 Owned 特性,表示其他对象如果引用了 Homework,就会自动建立主从关系—— Homework 为从属对象。

ForeignKey 特性指定外键的名称。虽然 StudentObj 属性的类型是 Student 类,但在建立数据表时,只引用了 Student 类的 StuID 属性。

此时,可以清空 OnModelCreating 方法中的代码了。

生成的数据表结构与上文差不多。

CREATE TABLE [dbo].[tb_students] (
[sID] INT IDENTITY (1, 1) NOT NULL,
[st_name] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_tb_students] PRIMARY KEY CLUSTERED ([sID] ASC)
); CREATE TABLE [dbo].[tb_homeworks] (
[wk_id] INT IDENTITY (1, 1) NOT NULL,
[wk_class] NVARCHAR (MAX) NULL,
[wk_sub] NVARCHAR (MAX) NULL,
[student_id] INT NULL,
CONSTRAINT [PK_tb_homeworks] PRIMARY KEY CLUSTERED ([wk_id] ASC),
CONSTRAINT [FK_tb_homeworks_tb_students_student_id] FOREIGN KEY ([student_id]) REFERENCES [dbo].[tb_students] ([sID])
);

当然了,最好的做法是将特性批注与 OnModelCreating  方法结合使用。

【EF Core】实体的主、从关系的更多相关文章

  1. EF Core 快速上手——EF Core的三种主要关系类型

    系列文章 EF Core 快速上手--EF Core 入门 本节导航 三种数据库关系类型建模 Migration方式创建和习修改数据库 定义和创建应用DbContext 将复杂查询拆分为子查询   本 ...

  2. Ef core 如何设置主键

    在正题之前,先说明几个问题. (1)写 sql 不好吗,为什么要引入 ORM ? 总的来说由于需求的复杂性增加,引入了面向对象编程,进而有了 ORM ,ORM 使得开发人员以对象的方式表达业务逻辑.对 ...

  3. EF Core 中多次从数据库查询实体数据,DbContext跟踪实体的情况

    使用EF Core时,如果多次从数据库中查询一个表的同一行数据,DbContext中跟踪(track)的实体到底有几个呢?我们下面就分情况讨论下. 数据库 首先我们的数据库中有一个Person表,其建 ...

  4. EF Core 2.1 支持数据库一对一关系

    在使用EF Core和设计数据库的时候,通常一对多.多对多关系使用得比较多,但是一对一关系使用得就比较少了.最近我发现实际上EF Core很好地支持了数据库的一对一关系. 数据库 我们先来看看SQL ...

  5. EF Core 2.0中如何手动映射数据库的视图为实体

    由于Scaffold-DbContext指令目前还不支持自动映射数据库中的视图为实体,所以当我们想使用EF Core来读取数据库视图数据的时候,我们需要手动去做映射,本文介绍如何在EF Core中手动 ...

  6. EF Core中如何通过实体集合属性删除从表的数据

    假设在数据库中有两个表:Person表和Book表,Person和Book是一对多关系 Person表数据: Book表数据: 可以看到数据库Book表中所有的数据都属于Person表中"F ...

  7. EF Core中怎么实现自动更新实体的属性值到数据库

    我们在开发系统的时候,经常会遇到这种需求数据库表中的行被更新时需要自动更新某些列. 数据库 比如下面的Person表有一列UpdateTime,这列数据要求在行被更新后自动更新为系统的当前时间. Pe ...

  8. EF Core怎么只Update实体的部分列数据

    下面是EF Core中的一个Person实体: public partial class Person { public int Id { get; set; } public string Code ...

  9. 把旧系统迁移到.Net Core 2.0 日记(10) -- EF core 和之前版本多对多映射区别

    EF Core 现在不支持多对多映射,只能做2个一对多映射. 比如Product和Category 我现在定义Product和Category是多对多关系. 那么实体定义如下: public clas ...

  10. EF Core中如何正确地设置两张表之间的关联关系

    数据库 假设现在我们在SQL Server数据库中有下面两张表: Person表,代表的是一个人: CREATE TABLE [dbo].[Person]( ,) NOT NULL, ) NULL, ...

随机推荐

  1. [Linux/JSON]JSON美化工具:json_pp / jq

    json_pp (git-bash内置的用于JSON格式化的管道工具:默认支持) (Linux CentOS7 暂不支持) curl http://localhost:8080/xxxx.json | ...

  2. 5.根据uid获取用户所有收货地址信息和设置默认地址

    1.总结:昨天主要是实现了根据uid获取该用户的所有收货地址以及设置默认地址:再对默认地址的实现里面让我认识到它的具体操作,首先我们根据aid查询收货地址 再根据收货地址查询到地址归属人的信息,判断u ...

  3. LeeCode 318周赛复盘

    T1: 对数组执行操作 思路:模拟 public int[] applyOperations(int[] nums) { int n = nums.length; for (int i = 0; i ...

  4. Python程序笔记20230302

    Alice.Bob 和他们的朋友们 问题主体 密码学家 Rivest.Shamir.Adleman 于1977年4月撰写了一篇论文<数字签名与公钥密码学>(On Digital Signa ...

  5. Redis缓冲区溢出及解决方案

    缓冲区(buffer),是内存空间的一部分.也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区. 一.Redis缓冲区溢出影响 在Redis ...

  6. JUC(一)JUC简介与Synchronized和Lock

    1 JUC简介 JUC就是java.util.concurrent的简称,这是一个处理线程的工具包,JDK1.5开始出现的. 进程和线程.管程 进程:系统资源分配的基本单位:它是程序的一次动态执行过程 ...

  7. Survivor

    Survivor (https://codeforces.com/group/L9GOcnr1dm/contest/422378/problem/F) 血的教训 比较有意思的一个贪心题 简单翻译一下题 ...

  8. GPT-4:思考的曙光还是数据的缩影?

    海盗分金,GPT-4初露锋芒 GPT系列模型横空出世后,其是否真实具有思考和推理的能力一直被业界关注.GPT-3.5在多条狗问题和海盗分金问题上表现糟糕.GPT-4在这两个谜题上给出的答案令人惊喜,甚 ...

  9. 设置Windows主机的浏览器为wls2的默认浏览器

    这里以Chrome为例. 1. 准备工作 wsl是可以使用Windows主机上安装的exe程序,出于安全考虑,默认情况下改功能是无法使用.要使用的话,终端需要以管理员权限启动. 我这里以Windows ...

  10. c语言趣味编程(1)百钱百鸡

    一.问题描述 百钱买百鸡问题:公鸡五文钱一只,母鸡三文钱一只,小鸡三只一文钱,用100文钱买100只鸡,公鸡.母鸡.小鸡各买多少只 二.设计思路 (1)定义三个变量下x,y,z代表公鸡,母鸡,小鸡的数 ...