• EFCore学习笔记(8)——关系


    关系

    关系(relationship)定义了两个实体(entity)之间是怎样联系的。
    在关系数据库中,这是由外键约束(foreign key constraint)来表示的。

    注意:
    本文中的大多数示例使用一对多的关系来说明概念。关于一对一的关系很简单,多对多关系的示例可以自行研究。 
    
    • 1
    • 2

    1. 术语的定义

    用来描述关系的术语有许多:

    这些术语对初学者来讲很头疼,但没办法,因为数据库这个东西,尤其是关系数据库,背后是有理论模型的,还有数学概念支撑。而这些理论模型、数学概念就是抽象的,它必然会产生一套概念、术语去描述它。所以在深入关系之前,有必要对这些概念有个大致了解。
    关系数据库模型中有许多概念与EF Core中类似,甚至等同。但是我学习的时候发现它们的英文是不一样的,比如关系数据库中主键是primary key,而EF Core中有principal key,两者虽然看似相等,但实际上不完全是一个东西。
    我想,作为初学者,应该谨慎对待。比如principal key,从微软文档来看,可能是主键或者候补键,那就不能和主键等同。

    • 依赖实体(Dependent entity):也叫从实体或子实体,这是一个包含外键属性的实体。有时也被称为关系的孩子。我个人喜欢叫它从实体,因为还有主实体,主从嘛,比较好记。
    • 主体实体(Principal entity):也叫主实体,这个实体包含了主键/候补键属性。有时候也称其为关系的父亲。

    这对实体结合父子、主从的概念来看都是很好理解的。从/子实体通过外键来找到主/父实体,进而访问主/父实体。因为从实体中的外键属性值,在主实体中必然存在,且为键。

    • 主体键(Principal key):这是一个唯一标识了主实体的属性。它可能是主键或候补键。我之所以称它为主体键,因为它是用来标识主实体的,而且这样命名可以与主键做区分。
    • 外键(Foreign key):它是存在于从实体中的属性,用于存储相关联实体(主实体)的主体键的值。

    外键:如果一个实体的某个字段指向另一个实体的主键,就称为外键。被指向的实体,称之为主实体,也叫父实体。负责指向的实体,称之为从实体,也叫子实体。从名字上也不难理解,外键就是外部(其他)实体的键。

    • 导航属性(Navigation property):这是一个定义在主实体或从实体中来引用相关实体的的属性。

    比如,我的博客中有许多文章,点击文章链接,就可以跳转到文章;又比如,我的文章中有标注所属的博客,我点击博客又可以跳转到我的博客信息。导航属性其实很简单,就是可以通过它跳转到别的实体上。就像导航栏的功能一样。

    • 导航属性还可细分为三种属性,
      • 集合导航属性(Collection navigation property):它是一个包含对许多相关实体引用的导航属性。(往往是一个集合,它引用了不止单个实体)
      • 引用导航属性(Reference navigation property):持有对单个相关实体的引用的导航属性。
      • 逆导航属性(Inverse navigation property):在讨论特定的导航属性时,这个术语指的是关系另一端的导航属性。
    • 自参照关系(Self-referencing relationship):依赖实体类型和主要实体类型相同的关系。

    以下代码展示了BlogPost之间一对多的关系:

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    其中,

    • Post是一个从实体。
      将上面两实体类用二维表的形式表示,
      在这里插入图片描述

    • Blog是主实体。

    • Blog.BlogId是主体键(该场景中,它是主键而不是候补键)。

    • Post.BlogId是外键。

    • Post.Blog是引用导航属性。

    • Blog.Posts是集合导航属性。

    • Post.BlogBlog.Posts的相反导航属性(反之亦然)。

    2. 约定(指按照EF Core规定默认配置)

    默认情况下,当在类型(指实体类)上发现导航属性时,就会创建关系。如果当前数据库提供程序无法将属性指向的类型映射为值类型(标量类型,scalar type),则该属性就被视为导航属性。(关系模型的域这个概念中有说到,域中元素,即属性的取值范围里的值都应该是原子的,而这边发现属性指向类型还能继续分,说明不符合域的原子性的约束,所以被数据库提供程序视为了导航属性)

    注意:
    按照约定发现的关系将始终以主实体的主键为目标。
    要以候补键为目标,必须使用fluent API执行额外的配置。

    2.1. 完全定义的关系

    关于关系,最常见的模式是在关系的两端都定义导航属性,并在从实体类中定义外键属性。

    • 如果在两个类型之间发现了一对导航属性,那么他们将配置为具有相同关系的相反导航属性。
    • 如果从实体包含了一个属性,该属性名与下面模式中的某个相匹配,则它被配置为外键:
      • <navigation property name><principal key property name>
        <导航属性名> + <主体键属性名>
      • <navigation property name>Id
        <导航属性名> + Id
      • <principal entity name><principal key property name>
        <主实体名> + <主体键属性名>
      • <principal entity name>Id
        <主实体名> + Id
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
    	// 1 导航属性
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
    	// 2 外键
        public int BlogId { get; set; }
        // 3 导航属性
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    本例中,标注的属性将被用于配置关系。

    注意:
    如果该属性是主键或其类型与主体键不兼容,则它不会被配置为外键。

    2.2. 无外键属性(的关系)

    尽管建议在从实体类中定义外键属性,但这不是必须的。如果没有找到外键属性,将会引入影子外键属性(shadow foreign key property),其名称为:

    • <navigation property name><principal key property name>
      <导航属性名> + <主体键属性名>
    • 或如果从实体类上没有导航属性时,
      <principal entity name><principal key property name>
      <主实体名> + <主体键属性名>。
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
    	// 集合导航属性
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
    	// 引用导航属性
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在本例中,影子外键是BlogId,因为前缀增加导航名显然是多余的(加了就变成了BlogBlogId)。

    注意:
    如果相同名称的属性已经存在,则影子属性名会加上数字后缀。

    2.3. 单个导航属性

    只包含一个导航属性(没有相反导航属性也没有外键属性)就足以由约定来定义关系。当然,你还可以添加导航属性和外键属性。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
    	// 集合导航属性
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.4. 局限性

    当在两个类型之间定义了多个导航属性时(就是说,不止一对导航指向彼此),由导航属性表示的关系比较模糊。你将需要手动配置它们来解决歧义。

    2.5. 级联删除(Cascade delete)

    按照约定,对于必需(required)的关系,级联删除将被设置为Cascade。对于可选的关系,设置为ClientSetNull
    Cascade意味着从实体也被删除。
    ClientSetNull表示没有加载到内存的从实体将保持不变,必须手动删除,或更新以指向有效的主体实体。对于加载到内存中的实体,EF Core将尝试将外键属性设置为空。

    3. 手动配置

    手动配置有两种方式,Fluent API和数据标注(Data annotations)。
    从我个人角度来看,数据标注的方式比较适合新手。不过这边还是两种都介绍下。

    • Fluent API:
    // Fluent API
    // 要在Fluent API中配置关系,首先要标识组成关系的导航属性
    // HasOne或HasMany用来标识实体类上的导航属性
    // 然后,链接调用到WithOne或WithMany来标识逆导航
    // HasOne/WithOne用于引用导航属性,HasMany/WithMany用于集合导航属性
    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Post>()	// 获取模型中的Post实体
                .HasOne(p => p.Blog)	// has,have有让、使的意思,可以理解为让某个属性成为导航属性
                .WithMany(b => b.Posts);// With有用的意思,可以理解为用某个属性指向该实体
                						// 至于One和Many,那就是数量上的区别了,引用和集合的区别
        }
    }
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public Blog Blog { get; set; }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 数据标注:
    // 你也可以使用数据标注来配置在从实体和主实体上导航属性配对的方式
    // 在两个实体之间有超过一对的导航属性时,会这么做。
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int AuthorUserId { get; set; }
        public User Author { get; set; }
    
        public int ContributorUserId { get; set; }
        public User Contributor { get; set; }
    }
    
    public class User
    {
        public string UserId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    
        [InverseProperty("Author")]
        public List<Post> AuthoredPosts { get; set; }
    
        [InverseProperty("Contributor")]
        public List<Post> ContributedToPosts { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    注意:
    你只能在从实体的属性上使用[Required]来影响关系的需求性。
    主实体导航属性上的[Required]通常被忽略(主体键本来就不能为空),但它可能导致实体变成依赖实体。

    注意:
    数据标注[ForeignKey]和[InverseProperty]在命名空间System.ComponentModel.DataAnnotations.Schema中可用。
    [Required]在命名空间System.ComponentModel.DataAnnotations中可用。

    3.1. 单个导航属性

    如果你只有一个导航属性,那么就可以用WithOneWithMany的无参重载。这表明在关系的另一端存在一个概念上的引用或集合,但在实体类中不包含导航属性。

    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>()
                .HasMany(b => b.Posts)
                .WithOne();
        }
    }
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    3.2. 配置导航属性

    注意:
    EF Core 5.0中引入的该特性。

    在创建导航属性后,你需要进一步配置它。

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne();
    
        modelBuilder.Entity<Blog>()
            .Navigation(b => b.Posts)
            .UsePropertyAccessMode(PropertyAccessMode.Property);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:
    此调用无法用于创建导航属性。它只用于配置导航属性,该属性是先前通过定义关系或根据约定创建的。(也就是说你得用其他方法先创建了导航属性,才能配置它)

    3.3. 外键

    • Fluent API(单键)
    // Fluent API (单键)
    // 你可以使用Fluent API来配置对于给定关系用作外键的属性:
    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Post>()
                .HasOne(p => p.Blog)
                .WithMany(b => b.Posts)
                .HasForeignKey(p => p.BlogForeignKey);
        }
    }
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogForeignKey { get; set; }
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • Fluent API(组合键)
    // 用Fluent API来配置给定关系的用作组合键的属性
    internal class MyContext : DbContext
    {
        public DbSet<Car> Cars { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Car>()
                .HasKey(c => new { c.State, c.LicensePlate });
    
            modelBuilder.Entity<RecordOfSale>()
                .HasOne(s => s.Car)
                .WithMany(c => c.SaleHistory)
                .HasForeignKey(s => new { s.CarState, s.CarLicensePlate });
        }
    }
    
    public class Car
    {
        public string State { get; set; }
        public string LicensePlate { get; set; }
        public string Make { get; set; }
        public string Model { get; set; }
    
        public List<RecordOfSale> SaleHistory { get; set; }
    }
    
    public class RecordOfSale
    {
        public int RecordOfSaleId { get; set; }
        public DateTime DateSold { get; set; }
        public decimal Price { get; set; }
    
        public string CarState { get; set; }
        public string CarLicensePlate { get; set; }
        public Car Car { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 数据标注(单键)
    // 你可以使用数据标注来配置给定关系中用作外键的属性。
    // 当按约定未发现外键属性时,通常会这么做:
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogForeignKey { get; set; }
    
        [ForeignKey("BlogForeignKey")]
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    提示:
    [ForeignKey]标注可以放在关系中的任意导航属性上。
    它不需要在从(依赖)实体类中的导航属性上。

    注意:
    在导航属性上使用[ForeignKey]指定的属性不需要存在于从实体类(依赖类)中。
    不存在的情况下,会创建指定的名称的影子外键。

    3.4. 影子外键

    你可以使用==HasForeignKey(…)==的字符串重载来配置一个影子属性作为外键。我们建议在使用影子属性作为外键之前,显式地将其添加到模型中(如下所示)。

    internal class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Add the shadow property to the model
            modelBuilder.Entity<Post>()
                .Property<int>("BlogForeignKey");
    
            // Use the shadow property as a foreign key
            modelBuilder.Entity<Post>()
                .HasOne(p => p.Blog)
                .WithMany(b => b.Posts)
                .HasForeignKey("BlogForeignKey");
        }
    }
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
  • 相关阅读:
    3.MySQL数据类型详解
    理解透彻API接口电商API接口有哪些?你需要一分钟看这篇文章
    Open3D(C++) ICP算法实现点云精配准
    [FMMPEG] parse与 demuxer
    L2-024 部落
    Educational Codeforces Round 119 (Rated for Div. 2)
    3LU6I是什么二极管
    【深度学习】求解偏导数
    【大数据入门核心技术-Hadoop】(四)Hadoop基础概念之YARN
    云原生技术在云计算中的应用探讨
  • 原文地址:https://blog.csdn.net/BadAyase/article/details/125558150