翼度科技»论坛 编程开发 .net 查看内容

【EF Core】实体的主、从关系

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
假设有以下两个实体:
  1. public class Student
  2. {
  3.     public int StuID { get; set; }
  4.     public string? Name { get; set; }
  5.     public IEnumerable<Homework>? Homeworks { get; set; }
  6. }
  7. public class Homework
  8. {
  9.     public string? Class { get; set; }
  10.     public string? Subject { get; set; }
  11. }
复制代码
Homework 类表示家庭作业,它并不是独立使用的,而是与学生类(Student)有依赖关系。一位学生有多个家庭作业记录,即 Homework 对象用于记录每位同学的作业的。按照这样的前提,Student 是主对象,Homework 是从对象。
Student 对象有个 Homeworks 属性,用于引用 Homework 对象,也就是所谓的“导航属性”。这个“导航”,估计意思就是你通过这个属性可以找到被引用的另一个实体对象,所以称之为导航,就是从 Navigation 的翻译。
随后,咱们要从 DbContext 类派生出自定义的数据库上下文。
  1. public class MyDbContext : DbContext
  2. {
  3.     // 映射的数据表,名称默认与属性名称一样
  4.     // 即 Students + Works
  5.     public DbSet<Student> Students => Set<Student>();
  6.     public DbSet<Homework> Works => Set<Homework>();
  7.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  8.     {
  9.         // 设置连接字符串
  10.         optionsBuilder.UseSqlServer(Helper.Conn_STRING);
  11.     }
  12.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  13.     {
  14.         // 设置主键
  15.         modelBuilder.Entity<Student>().HasKey(s => s.StuID);
  16.         // 建立主从关系
  17.         modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
  18.     }
  19. }
复制代码
连接字符串是老周事先配置好的,连的是 SQL Server。
  1. public class Helper
  2. {
  3.     public const string Conn_STRING = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True";
  4. }
复制代码
用的是 LocalDB,这玩意儿方便。
其实这是一个控制台应用程序,并添加了 Nuget 包。
  1.   <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </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、往数据库中存一点数据。
  1. static void Main(string[] args){    using (var ctx = new MyDbContext())    {        //ctx.Database.EnsureDeleted();        bool res = ctx.Database.EnsureCreated();        if (res)        {  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>Console.WriteLine("已创建数据库");        }    }    using(MyDbContext ctx = new())    {        // 加点料        ctx.Students.Add(new Student        {  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>Name = "小张",  <ItemGroup>
  8.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  9.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  10.   </ItemGroup>Homeworks = new List  <ItemGroup>
  11.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  12.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  13.   </ItemGroup>{  <ItemGroup>
  14.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  15.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  16.   </ItemGroup>    new Homework{ Class = "数学", Subject = "3000道口算题"},  <ItemGroup>
  17.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  18.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  19.   </ItemGroup>    new Homework{ Class = "英语", Subject = "背9999个单词"}  <ItemGroup>
  20.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  21.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  22.   </ItemGroup>}        });        ctx.Students.Add(new Student        {  <ItemGroup>
  23.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  24.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  25.   </ItemGroup>Name = "小雪",  <ItemGroup>
  26.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  27.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  28.   </ItemGroup>Homeworks = new Homework[]  <ItemGroup>
  29.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  30.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  31.   </ItemGroup>{  <ItemGroup>
  32.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  33.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  34.   </ItemGroup>    new Homework{ Class = "历史", Subject = "临一幅《清明上河图》"},  <ItemGroup>
  35.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  36.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  37.   </ItemGroup>    new Homework{ Class = "语文", Subject = "作文题:《百鬼日行》"}  <ItemGroup>
  38.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  39.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  40.   </ItemGroup>}        });        // 保存        int x = ctx.SaveChanges();        Console.WriteLine("共保存了{0}条记录", x);    }}
复制代码
EnsureCreated 方法会自动创建数据库。如果不存在数据库且创建成功,返回 true,否则是 false。数据库的名称在连接字符串中配置过。
  1. Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=stuDB;Integrated Security=True
复制代码
接下来,我们运行一下。稍等几秒钟,看到控制台输出下面文本就算成功了。
  1. 已创建数据库
  2. 共保存了6条记录
复制代码
然后,连上去看看有没有数据库。

看看,这表的名称是不是和 MyDbContext 的两个属性一样? 
  1. public class MyDbContext : DbContext
  2. {
  3.     public DbSet<Student> Students => Set<Student>();
  4.     public DbSet<Homework> Works => Set<Homework>();
  5.     ……
复制代码
你要是不喜欢用这俩名字,也可以发动传统技能(指老 EF),用 Table 特性给它们另取高名。
  1. [Table("tb_students", Schema = "dbo")]
  2. public class Student
  3. {
  4.    ……
  5. }
  6. [Table("tb_homeworks", Schema = "dbo")]
  7. public class Homework
  8. {
  9.     ……
  10. }
复制代码
删除数据库,再运行一次程序,然后再登录数据库看看,表名变了吗?

那有伙伴们会问:有没有现代技能?有的,使用 ToTable 方法定义映射的数据表名称。
先去掉 Student、Homework 类上的 Table 特性,然后直接在重写 OnModelCreating 方法时配置。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3.     modelBuilder.Entity<Student>().ToTable("dt_students").HasKey(s => s.StuID);
  4.     modelBuilder.Entity<Homework>().ToTable("dt_works");
  5.     // 建立主从关系
  6.     modelBuilder.Entity<Student>().OwnsMany(s => s.Homeworks);
  7. }
复制代码
但是这样写会报错的。因为 Homework 实体是 Student 的从属对象,单独调用 ToTable 方法在配置的时候会将其设置为独立对象,而非从属对象。
所以,正确的做法是在两个实体建立了从属性关系后再调用 ToTable 方法(Student 对象是主对象,它可以单独调用)。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3.     modelBuilder.Entity<Student>().HasKey(s => s.StuID);
  4.     modelBuilder.Entity<Student>()
  5.         .ToTable("tb_students")
  6.         .OwnsMany(s => s.Homeworks)
  7.         .ToTable("tb_works");
  8. }
复制代码
因为 Homework 是 Student 的从属,tb_works 表中要存在一个外键——引用 Student.StuID,这样两个表才能建立主从关系。如果单独调用 Entity.ToTable 映射表的话,那么表中不会添加引用 StuID 的外键列。就是默认被配置为非主从模式。没有了外键,tb_works 表中存的数据就无法知道是哪位学生的作业了。
这样创建数据库后,tb_works 表中就存在名为 StudentStuID 的列,它就是引用 Student.StuID 的外键。
  1. CREATE TABLE [dbo].[tb_works] (    [StudentStuID] INT  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>NOT NULL,    [Id]           INT  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>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)生成的,如果你想自己搞个名字,也是可以的。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity().HasKey(s => s.StuID);    modelBuilder.Entity()        .ToTable("tb_students")        .OwnsMany(s => s.Homeworks, tb =>        {  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>tb.ToTable("tb_works");  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>tb.WithOwner().HasForeignKey("student_id");        });}
复制代码
这样 tb_works 表中就有了名为 student_id 的外键。
  1. CREATE TABLE [dbo].[tb_works] (    [student_id] INT  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>NOT NULL,    [Id]         INT  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>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 方法是指:俺是从表,我要配置一下和主表之间建立联系的参数(如上面给外键另起个名字)。
那么,我想把两个表的列全自定义命名,可以吗?当然可以的。
  1. protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity().HasKey(s => s.StuID);    modelBuilder.Entity()        .ToTable("tb_students", tb =>        {  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>tb.Property(s => s.StuID).HasColumnName("sID");  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>tb.Property(s => s.Name).HasColumnName("stu_name");        })        .OwnsMany(s => s.Homeworks, tb =>        {  <ItemGroup>
  8.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  9.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  10.   </ItemGroup>tb.ToTable("tb_works");  <ItemGroup>
  11.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  12.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  13.   </ItemGroup>tb.WithOwner().HasForeignKey("student_id");  <ItemGroup>
  14.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  15.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  16.   </ItemGroup>tb.Property(w => w.Class).HasColumnName("wk_class");  <ItemGroup>
  17.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  18.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  19.   </ItemGroup>tb.Property(w => w.Subject).HasColumnName("wk_sub");        });}
复制代码
两个表的字段名都变了。
  1. CREATE TABLE [dbo].[tb_students] (    [sID]      INT  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>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  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>NOT NULL,    [Id]         INT  <ItemGroup>
  8.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  9.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  10.   </ItemGroup>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 方法中建模我头有点晕,我能不能在定义实体类的时候,直接通过特性批注来实现主从关系呢?那肯定可以的了。
  1. [Table("tb_students")]
  2. [PrimaryKey(nameof(StuID))]
  3. public class Student
  4. {
  5.     [Column("sID")]
  6.     public int StuID { get; set; }
  7.     [Column("st_name")]
  8.     public string? Name { get; set; }
  9.     // 这是导航属性,不需要映射到数据表
  10.     public IEnumerable<Homework>? Homeworks { get; set; }
  11. }
  12. [Owned]
  13. [Table("tb_homeworks")]
  14. [PrimaryKey(nameof(wID))]
  15. public class Homework
  16. {
  17.     [Column("wk_id")]
  18.     public int wID { get; set; }
  19.     [Column("wk_class")]
  20.     public string? Class { get; set; }
  21.     [Column("wk_sub")]
  22.     public string? Subject { get; set; }
  23.     [ForeignKey("student_id")]  //设置外键名称
  24.     public Student? StudentObj { get; set; }
  25. }
复制代码
PrimaryKey 特性设置实体类中哪些属性为主键,使用属性成员的名称,而不是数据表字段名称。
在 Homework 类上用到 Owned 特性,表示其他对象如果引用了 Homework,就会自动建立主从关系—— Homework 为从属对象。
ForeignKey 特性指定外键的名称。虽然 StudentObj 属性的类型是 Student 类,但在建立数据表时,只引用了 Student 类的 StuID 属性。
此时,可以清空 OnModelCreating 方法中的代码了。
生成的数据表结构与上文差不多。
  1. CREATE TABLE [dbo].[tb_students] (    [sID]     INT  <ItemGroup>
  2.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  3.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  4.   </ItemGroup>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  <ItemGroup>
  5.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  6.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  7.   </ItemGroup>IDENTITY (1, 1) NOT NULL,    [wk_class]   NVARCHAR (MAX) NULL,    [wk_sub]     NVARCHAR (MAX) NULL,    [student_id] INT  <ItemGroup>
  8.     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
  9.     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
  10.   </ItemGroup>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  方法结合使用。

来源:https://www.cnblogs.com/tcjiaan/archive/2023/07/02/17520679.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具