关系

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

注意

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

术语的定义

有许多用于描述关系的术语

  • 依赖实体:这是包含外键属性的实体。 有时是指关系的“子级”。

  • 主体实体:这是包含主键/备选键属性的实体。 有时是指关系的“父级”。

  • 主体键:唯一标识主体实体的属性。 它可能是主键,也可能是备选键。

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

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

    • 集合导航属性:包含对许多相关实体的引用的导航属性。

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

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

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

以下代码展示了 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; }
}
  • Post 是依赖实体

  • Blog 是主体实体

  • Blog.BlogId 是主体键(在此示例中,它是主键,而不是备选键)

  • Post.BlogId 是外键

  • Post.Blog 是引用导航属性

  • Blog.Posts 是集合导航属性

  • Post.BlogBlog.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; }
}

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

注意

如果属性是主键或属性的类型与主体键不兼容,则不会将其配置为外键。

无外键属性

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

限制

如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航),则由导航属性表示的关系是不明确的。 需要对它们进行手动配置才能解决这种不明确的关系。

级联删除

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

有关必需关系和可选关系的区别,请参阅必需关系和可选关系部分。

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

手动配置

若要在 Fluent API 中配置关系,首先应标识构成关系的导航属性。 HasOneHasMany 标识要开始配置的实体类型的导航属性。 然后,将调用链接到 WithOneWithMany 以标识反向导航。 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; }
}

外键约束名称

根据约定,当以关系数据库作为目标时,外键约束将命名为 FK__<依赖类型名称>_<主体类型名称>_<外键属性名称>。 对于复合外键,<外键属性名称> 将成为外键属性名称的下划线分隔列表。

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

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

主体键

如果想要外键引用主键外的属性,可使用 Fluent 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 显式配置给定关系的级联删除行为。

有关每个选项的详细介绍,请参阅级联删除

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

在数据库中实现此关系的方式是使用联接表,其中包含 PostTag 的外键。 例如,以下就是 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>,如下一节所述。

多对多导航称为跳过导航,因为它们有效地跳过联接实体类型。 如果要使用批量配置,则可以从 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));

注意

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

间接多对多关系

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

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

注意

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

其他资源