2

【EF Core】实体的主、从关系 - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/17520679.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

假设有以下两个实体:

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条记录

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

367389-20230702115103470-686696743.png

看看,这表的名称是不是和 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
{
    ……
}

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

367389-20230702120020131-822362774.png

那有伙伴们会问:有没有现代技能?有的,使用 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  方法结合使用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK