关系

关系定义两个实体之间的关系。 在关系数据库中,这由外键约束表示。

备注

本文中的大多数示例都使用一对多关系来演示概念。 有关一对一关系和多对多关系的示例,请参阅文章末尾的 其他关系模式 部分。

术语定义

有许多术语用于描述关系

  • 相关实体: 这是包含外键属性的实体。 有时称为关系的 "子级"。

  • 主体实体: 这是包含主/备用键属性的实体。 有时称为关系的 "父项"。

  • 主体密钥: 唯一标识主体实体的属性。 这可能是主键或备用密钥。

  • 外键: 用于存储相关实体的主体键值的依赖实体中的属性。

  • 导航属性: 在主体和/或从属实体上定义的属性,该属性引用相关实体。

    • 集合导航属性: 一个导航属性,其中包含对多个相关实体的引用。

    • 引用导航属性: 保存对单个相关实体的引用的导航属性。

    • 反向导航属性: 讨论特定导航属性时,此术语是指关系另一端的导航属性。

  • 自引用关系: 依赖关系和主体实体类型相同的关系。

以下代码演示 了 和 之间的一对多 Blog 关系 Post

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; }
}
  • Post 是依赖实体

  • Blog 是主体实体

  • Blog.BlogId 是主密钥 (,在这种情况下,它是主密钥,而不是备用)

  • Post.BlogId 是外键

  • Post.Blog 是引用导航属性

  • Blog.Posts 是集合导航属性

  • Post.Blog 是 (的反向导航属性 Blog.Posts ,反之亦然)

约定

默认情况下,当在类型上发现导航属性时,将创建关系。 如果属性指向的类型无法由当前数据库提供程序映射为标量类型,则该属性被视为导航属性。

备注

按约定发现的关系将始终以主体实体的主键为目标。 若要以备用密钥为目标,必须使用 Fluent API 执行其他配置。

完全定义的关系

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

  • 如果在两种类型之间找到了一对导航属性,则它们将被配置为相同关系的反向导航属性。

  • 如果依赖实体包含一个名称与以下模式之一匹配的属性,则它将配置为外键:

    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity name><principal key property name>
    • <principal entity name>Id
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; }
}

本示例使用突出显示的属性来配置关系。

备注

如果属性是主键或类型与主体密钥不兼容,则它不会配置为外键。

备注

在 EF Core 3.0 之前,与主体键属性完全相同的属性也匹配 为外键

没有外键属性

尽管建议在依赖实体类中定义外键属性,但这并不是必需的。 如果未找到外键属性,则会使用名称引入 阴影外键属性<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; }
}

在此示例中,阴影外键是 BlogId 因为预先计算导航名称将是冗余的。

备注

如果已存在具有相同名称的属性,则会以数字作为后缀的阴影属性名称。

单个导航属性

只包含一个导航属性 (不会反向导航,并且没有外键属性) 足以具有约定定义的关系。 还可以有一个导航属性和一个外键属性。

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; }
}

限制

如果在两个类型之间定义了多个导航属性 (也就是说,多个只是一对导航) 导航属性表示的关系是不明确的。 你将需要手动对其进行配置以解决歧义。

级联删除

按照约定,级联删除将对所需的关系和 ClientSetNull 设置为 cascade ,以实现可选关系。 Cascade 表示也会删除依赖实体。 ClientSetNull 表示未加载到内存中的依赖实体将保持不变,必须手动删除,或将其更新为指向有效的主体实体。 对于加载到内存中的实体,EF Core 将尝试将外键属性设置为 null。

请参阅 required 和 optional 关系部分,了解必需和可选关系之间的差异。

有关不同的删除行为和约定使用的默认值的详细信息,请参阅 级联删除

手动配置

若要在熟知的 API 中配置关系,请首先标识构成关系的导航属性。 HasOneHasMany 标识要开始配置的实体类型的导航属性。 然后,将调用链接 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>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts);
    }
}

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; }
}

单个导航属性

如果只有一个导航属性,则 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; }
}

配置导航属性

备注

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);
}

备注

此调用不能用于创建导航属性。 它仅用于配置之前通过定义关系或约定创建的导航属性。

外键

可以使用 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; }
}

影子外键

您可以使用的字符串重载将 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; }
}

Foreign key 约束名称

按照约定,以关系数据库为目标时,外键约束命名为 FK _ <dependent type name> _ <principal type name> _ <foreign key property name> 。 对于复合外键, <foreign key property name> 将成为外键属性名称的下划线分隔列表。

你还可以配置约束名称,如下所示:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .HasForeignKey(p => p.BlogId)
        .HasConstraintName("ForeignKey_Post_Blog");
}

无导航属性

不一定需要提供导航属性。 您可以直接在关系的一端提供外键。

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<Blog>()
            .WithMany()
            .HasForeignKey(p => p.BlogId);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { 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; }
}

主体密钥

如果你希望外键引用主键之外的属性,则可以使用熟知的 API 来配置关系的主体键属性。 你配置为主体密钥的属性将自动设置为 备用密钥

internal class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<RecordOfSale>()
            .HasOne(s => s.Car)
            .WithMany(c => c.SaleHistory)
            .HasForeignKey(s => s.CarLicensePlate)
            .HasPrincipalKey(c => c.LicensePlate);
    }
}

public class Car
{
    public int CarId { 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 CarLicensePlate { get; set; }
    public Car Car { get; set; }
}

必需和可选关系

可以使用 Fluent API 来配置关系是必需还是可选。 最终,这控制外键属性是必需还是可选。 使用阴影状态外键时,这非常有用。 如果实体类中具有外键属性,则关系的必需性取决于外键属性是必需还是可选 (请参阅必需 和可选属性,了解详细信息) 。

外键属性位于依赖实体类型上,因此,如果它们按要求进行配置,则意味着每个依赖实体都需要具有相应的主体实体。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .IsRequired();
}

备注

除非 IsRequired(false) 其他配置外键属性,否则调用 也会使外键属性成为可选属性。

级联删除

可以使用 Fluent API 显式配置给定关系的级联删除行为。

有关 每个选项 的详细介绍,请参阅 Cascade Delete。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .OnDelete(DeleteBehavior.Cascade);
}

其他关系模式

一对一

一对一关系在两侧都有引用导航属性。 它们遵循与一对多关系相同的约定,但外键属性上引入了唯一索引,以确保只有一个依赖项与每个主体相关。

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

备注

EF 将基于其检测外键属性的能力,选择其中一个实体作为依赖实体。 如果选择错误的实体作为依赖实体,可以使用 Fluent API 更正此错误。

配置与 Fluent API 的关系时,请使用 HasOneWithOne 方法。

配置外键时,需要指定依赖实体类型 - 请注意下面列表中提供给 的 HasForeignKey 泛型参数。 在一对多关系中,具有引用导航的实体是依赖实体,具有集合的实体是主体。 但这并不是一对一的关系,因此需要显式定义它。

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<BlogImage> BlogImages { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasOne(b => b.BlogImage)
            .WithOne(i => i.Blog)
            .HasForeignKey<BlogImage>(b => b.BlogForeignKey);
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
    public int BlogImageId { get; set; }
    public byte[] Image { get; set; }
    public string Caption { get; set; }

    public int BlogForeignKey { get; set; }
    public Blog Blog { get; set; }
}

默认情况下,从属端被视为可选的,但可以根据需要进行配置。 但是,EF 不会验证是否提供了依赖实体,因此,只有在数据库映射允许强制执行此配置时,此配置才会产生差别。 这种情况的常见方案是默认情况下使用表拆分的引用拥有的类型。

modelBuilder.Entity<Order>(
    ob =>
    {
        ob.OwnsOne(
            o => o.ShippingAddress,
            sa =>
            {
                sa.Property(p => p.Street).IsRequired();
                sa.Property(p => p.City).IsRequired();
            });

        ob.Navigation(o => o.ShippingAddress)
            .IsRequired();
    });

对于此配置,与对应的列 ShippingAddress 在数据库中将标记为不可为 null。

备注

如果使用 不可为 null 的引用类型 IsRequired ,则无需调用。

备注

EF Core 5.0 中引入了配置依赖是否必需的功能。

多对多

多对多关系要求两侧都有集合导航属性。 它们将被类似于其他类型的关系的惯例来发现。

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection<Post> Posts { get; set; }
}

此关系在数据库中的实现方式是通过联接表,其中包含和的外键 Post Tag 。 例如,这是 EF 将在上述模型的关系数据库中创建的内容。

CREATE TABLE [Posts] (
    [PostId] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Content] nvarchar(max) NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);

CREATE TABLE [Tags] (
    [TagId] nvarchar(450) NOT NULL,
    CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] nvarchar(450) NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);

在内部,EF 会创建一个实体类型来表示将被称为联接实体类型的联接表。 Dictionary<string, object> 当前用于处理外键属性的任意组合,有关详细信息,请参阅 属性包实体类型 。 模型中可能存在多个多对多关系,因此,在此情况下,必须为联接实体类型指定唯一名称 PostTag 。 此功能可用于共享类型的实体类型。

重要

用于按约定联接实体类型的 CLR 类型在将来的版本中可能会更改以提高性能。 不要依赖于联接类型 Dictionary<string, object> ,除非已显式配置了此项,如下一节中所述。

"多对多" 导航称为 "跳过导航",因为它们实际上会跳过联接实体类型。 如果你正在运用大容量配置,则可以从获取所有 skip 导航 GetSkipNavigations

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    foreach (var skipNavigation in entityType.GetSkipNavigations())
    {
        Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
    }
}

联接实体类型配置

通常将配置应用于联接实体类型。 此操作可以通过 完成 UsingEntity

modelBuilder
    .Entity<Post>()
    .HasMany(p => p.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity(j => j.ToTable("PostTags"));

可以使用匿名 类型为联接实体类型提供模型种子数据。 可以检查模型调试视图以确定按约定创建的属性名称。

modelBuilder
    .Entity<Post>()
    .HasData(new Post { PostId = 1, Title = "First" });

modelBuilder
    .Entity<Tag>()
    .HasData(new Tag { TagId = "ef" });

modelBuilder
    .Entity<Post>()
    .HasMany(p => p.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity(j => j.HasData(new { PostsPostId = 1, TagsTagId = "ef" }));

其他数据存储在联接实体类型中,但为此,最好创建定制的 CLR 类型。 使用自定义联接实体类型配置关系时,需要显式指定这两个外键。

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options)
        : base(options)
    {
    }

    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasMany(p => p.Tags)
            .WithMany(p => p.Posts)
            .UsingEntity<PostTag>(
                j => j
                    .HasOne(pt => pt.Tag)
                    .WithMany(t => t.PostTags)
                    .HasForeignKey(pt => pt.TagId),
                j => j
                    .HasOne(pt => pt.Post)
                    .WithMany(p => p.PostTags)
                    .HasForeignKey(pt => pt.PostId),
                j =>
                {
                    j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
                    j.HasKey(t => new { t.PostId, t.TagId });
                });
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection<Tag> Tags { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection<Post> Posts { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

联接关系配置

EF 对联接实体类型使用两个一对多关系来表示多对多关系。 可以在参数中配置这些 UsingEntity 关系。

modelBuilder.Entity<Post>()
    .HasMany(p => p.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        j => j
            .HasOne<Tag>()
            .WithMany()
            .HasForeignKey("TagId")
            .HasConstraintName("FK_PostTag_Tags_TagId")
            .OnDelete(DeleteBehavior.Cascade),
        j => j
            .HasOne<Post>()
            .WithMany()
            .HasForeignKey("PostId")
            .HasConstraintName("FK_PostTag_Posts_PostId")
            .OnDelete(DeleteBehavior.ClientCascade));

备注

5.0 版中引入了配置多对多关系EF Core,对于以前的版本,请使用以下方法。

间接多对多关系

还可以表示多对多关系,只需添加联接实体类型并映射两个单独的一对多关系。

public class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options)
        : base(options)
    {
    }

    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PostTag>()
            .HasKey(t => new { t.PostId, t.TagId });

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Post)
            .WithMany(p => p.PostTags)
            .HasForeignKey(pt => pt.PostId);

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Tag)
            .WithMany(t => t.PostTags)
            .HasForeignKey(pt => pt.TagId);
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

备注

尚未添加对数据库中多对多关系搭建基架的支持。 请参阅跟踪问题

其他资源