关系Relationships

关系定义两个实体之间的关系。A relationship defines how two entities relate to each other. 在关系数据库中,这由外键约束表示。In a relational database, this is represented by a foreign key constraint.

备注

本文中的大多数示例都使用一对多关系来演示概念。Most of the samples in this article use a one-to-many relationship to demonstrate concepts. 有关一对一关系和多对多关系的示例,请参阅文章末尾的其他关系模式部分。For examples of one-to-one and many-to-many relationships see the Other Relationship Patterns section at the end of the article.

术语定义Definition of terms

有许多术语用于描述关系There are a number of terms used to describe relationships

  • 相关实体: 这是包含外键属性的实体。Dependent entity: This is the entity that contains the foreign key properties. 有时称为关系的 "子级"。Sometimes referred to as the 'child' of the relationship.

  • 主体实体: 这是包含主/备用键属性的实体。Principal entity: This is the entity that contains the primary/alternate key properties. 有时称为关系的 "父项"。Sometimes referred to as the 'parent' of the relationship.

  • 主体密钥: 唯一标识主体实体的属性。Principal key: The properties that uniquely identify the principal entity. 这可能是主键或备用密钥。This may be the primary key or an alternate key.

  • 外键: 用于存储相关实体的主体键值的依赖实体中的属性。Foreign key: The properties in the dependent entity that are used to store the principal key values for the related entity.

  • 导航属性: 在主体和/或从属实体上定义的属性,该属性引用相关实体。Navigation property: A property defined on the principal and/or dependent entity that references the related entity.

    • 集合导航属性: 一个导航属性,其中包含对多个相关实体的引用。Collection navigation property: A navigation property that contains references to many related entities.

    • 引用导航属性: 保存对单个相关实体的引用的导航属性。Reference navigation property: A navigation property that holds a reference to a single related entity.

    • 反向导航属性: 讨论特定导航属性时,此术语是指关系另一端的导航属性。Inverse navigation property: When discussing a particular navigation property, this term refers to the navigation property on the other end of the relationship.

  • 自引用关系: 依赖关系和主体实体类型相同的关系。Self-referencing relationship: A relationship in which the dependent and the principal entity types are the same.

下面的代码显示与之间的一对多关系 Blog``PostThe following code shows a one-to-many relationship between Blog and 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是依赖实体Post is the dependent entity

  • Blog是主体实体Blog is the principal entity

  • Blog.BlogId是主体键(在本例中为主密钥,而不是备用密钥)Blog.BlogId is the principal key (in this case it is a primary key rather than an alternate key)

  • Post.BlogId为外键Post.BlogId is the foreign key

  • Post.Blog是一个引用导航属性Post.Blog is a reference navigation property

  • Blog.Posts是集合导航属性Blog.Posts is a collection navigation property

  • Post.Blog是的反向导航属性 Blog.Posts (反之亦然)Post.Blog is the inverse navigation property of Blog.Posts (and vice versa)

约定Conventions

默认情况下,当在某个类型上发现导航属性时,将创建一个关系。By default, a relationship will be created when there is a navigation property discovered on a type. 如果当前数据库提供程序无法将其指向的类型映射为标量类型,则该属性被视为导航属性。A property is considered a navigation property if the type it points to can not be mapped as a scalar type by the current database provider.

备注

按约定发现的关系将始终以主体实体的主键为目标。Relationships that are discovered by convention will always target the primary key of the principal entity. 若要以备用密钥为目标,则必须使用熟知的 API 执行其他配置。To target an alternate key, additional configuration must be performed using the Fluent API.

完全定义的关系Fully defined relationships

关系最常见的模式是在关系两端定义导航属性,在依赖实体类中定义外键属性。The most common pattern for relationships is to have navigation properties defined on both ends of the relationship and a foreign key property defined in the dependent entity class.

  • 如果在两个类型之间找到一对导航属性,则这些属性将配置为同一关系的反向导航属性。If a pair of navigation properties is found between two types, then they will be configured as inverse navigation properties of the same relationship.

  • 如果依赖实体包含名称与其中一种模式相匹配的属性,则该属性将被配置为外键:If the dependent entity contains a property with a name matching one of these patterns then it will be configured as the foreign key:

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

在此示例中,突出显示的属性将用于配置关系。In this example the highlighted properties will be used to configure the relationship.

备注

如果属性为主键,或者为与主体键不兼容的类型,则不会将其配置为外键。If the property is the primary key or is of a type not compatible with the principal key then it won't be configured as the foreign key.

备注

在 EF Core 3.0 之前,名为与主体键属性完全相同的属性也与外键匹配Before EF Core 3.0 the property named exactly the same as the principal key property was also matched as the foreign key

无外键属性No foreign key property

尽管建议在依赖实体类中定义外键属性,但这并不是必需的。While it is recommended to have a foreign key property defined in the dependent entity class, it is not required. 如果未找到外键属性,则会使用名称引入阴影外键属性<navigation property name><principal key property name> <principal entity name><principal key property name> 如果依赖类型上没有导航,则为。If no foreign key property is found, a shadow foreign key property will be introduced with the name <navigation property name><principal key property name> or <principal entity name><principal key property name> if no navigation is present on the dependent type.

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 因为预先计算导航名称将是冗余的。In this example the shadow foreign key is BlogId because prepending the navigation name would be redundant.

备注

如果已存在具有相同名称的属性,则会以数字作为后缀的阴影属性名称。If a property with the same name already exists then the shadow property name will be suffixed with a number.

单个导航属性Single navigation property

只包含一个导航属性(无反向导航,没有外键属性)就足以具有约定定义的关系。Including just one navigation property (no inverse navigation, and no foreign key property) is enough to have a relationship defined by convention. 还可以有一个导航属性和一个外键属性。You can also have a single navigation property and a foreign key property.

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

限制Limitations

如果有多个在两种类型之间定义的导航属性(即,不仅仅是一对相互指向的导航属性),则导航属性表示的关系是不明确的。When there are multiple navigation properties defined between two types (that is, more than just one pair of navigations that point to each other) the relationships represented by the navigation properties are ambiguous. 你将需要手动对其进行配置以解决歧义。You will need to manually configure them to resolve the ambiguity.

级联删除Cascade delete

按照约定,级联删除将对所需的关系和ClientSetNull设置为cascade ,以实现可选关系。By convention, cascade delete will be set to Cascade for required relationships and ClientSetNull for optional relationships. Cascade表示也会删除依赖实体。Cascade means dependent entities are also deleted. ClientSetNull表示未加载到内存中的依赖实体将保持不变,必须手动删除,或将其更新为指向有效的主体实体。ClientSetNull means that dependent entities that are not loaded into memory will remain unchanged and must be manually deleted, or updated to point to a valid principal entity. 对于加载到内存中的实体,EF Core 将尝试将外键属性设置为 null。For entities that are loaded into memory, EF Core will attempt to set the foreign key properties to null.

请参阅 required和 optional关系部分,了解必需和可选关系之间的差异。See the Required and Optional Relationships section for the difference between required and optional relationships.

有关不同的删除行为和约定使用的默认值的详细信息,请参阅级联删除See Cascade Delete for more details about the different delete behaviors and the defaults used by convention.

手动配置Manual configuration

若要在熟知的 API 中配置关系,请首先标识构成关系的导航属性。To configure a relationship in the Fluent API, you start by identifying the navigation properties that make up the relationship. HasOneHasMany 标识要开始配置的实体类型上的导航属性。HasOne or HasMany identifies the navigation property on the entity type you are beginning the configuration on. 然后,将调用链接到 WithOneWithMany 以标识反向导航。You then chain a call to WithOne or WithMany to identify the inverse navigation. HasOne/WithOne用于引用导航属性,用于 HasMany / WithMany 集合导航属性。HasOne/WithOne are used for reference navigation properties and HasMany/WithMany are used for collection navigation properties.

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

单个导航属性Single navigation property

如果只有一个导航属性,则和的无参数重载 WithOne WithManyIf you only have one navigation property then there are parameterless overloads of WithOne and WithMany. 这表示在概念上,关系的另一端有一个引用或集合,但实体类中不包含导航属性。This indicates that there is conceptually a reference or collection on the other end of the relationship, but there is no navigation property included in the entity class.

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

配置导航属性Configuring navigation properties

创建导航属性后,你可能需要对其进行进一步配置。After the navigation property has been created, you may need to further configure it. 在 EFCore 5.0 中,添加了新的流畅 API,使你可以执行该配置。In EFCore 5.0, new Fluent API is added to allow you to perform that configuration.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(b => b.Posts)
        .WithOne();

    modelBuilder.Entity<Blog>()
        .Navigation(b => b.Posts)
            .UsePropertyAccessMode(PropertyAccessMode.Property);
}

备注

此调用不能用于创建导航属性。This call cannot be used to create a navigation property. 它仅用于配置导航属性,该属性以前是通过定义关系或从约定创建的。It is only used to configure a navigation property which has been previously created by defining a relationship or from a convention.

外键Foreign key

您可以使用熟知的 API 来配置应用作给定关系的外键属性的属性:You can use the Fluent API to configure which property should be used as the foreign key property for a given relationship:

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

影子外键Shadow foreign key

您可以使用的字符串重载将 HasForeignKey(...) 影子属性配置为外键(有关详细信息,请参阅影子属性)。You can use the string overload of HasForeignKey(...) to configure a shadow property as a foreign key (see Shadow Properties for more information). 建议先将影子属性显式添加到模型,然后再将其用作外键(如下所示)。We recommend explicitly adding the shadow property to the model before using it as a foreign key (as shown below).

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 约束名称Foreign key constraint name

按照约定,在面向关系数据库时,外键约束命名为 FK_ By convention, when targeting a relational database, foreign key constraints are named FK_. 对于复合外键, 将成为外键属性名称的下划线分隔列表。For composite foreign keys becomes an underscore separated list of foreign key property names.

你还可以配置约束名称,如下所示:You can also configure the constraint name as follows:

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

无导航属性Without navigation property

不一定需要提供导航属性。You don't necessarily need to provide a navigation property. 您可以直接在关系的一端提供外键。You can simply provide a foreign key on one side of the relationship.

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

主体密钥Principal key

如果你希望外键引用主键之外的属性,则可以使用熟知的 API 来配置关系的主体键属性。If you want the foreign key to reference a property other than the primary key, you can use the Fluent API to configure the principal key property for the relationship. 配置为主体密钥的属性将自动设置为备用密钥The property that you configure as the principal key will automatically be setup as an alternate key.

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

必需和可选的关系Required and optional relationships

您可以使用熟知的 API 来配置关系是必需的还是可选的。You can use the Fluent API to configure whether the relationship is required or optional. 最终,这会控制外键属性是必需的还是可选的。Ultimately this controls whether the foreign key property is required or optional. 当使用阴影状态外键时,这非常有用。This is most useful when you are using a shadow state foreign key. 如果实体类中具有外键属性,则关系的 requiredness 取决于外键属性是必需还是可选(有关详细信息,请参阅必需和可选属性)。If you have a foreign key property in your entity class then the requiredness of the relationship is determined based on whether the foreign key property is required or optional (see Required and Optional properties for more information).

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

备注

IsRequired(false)如果未配置,则调用还会使外键属性为可选。Calling IsRequired(false) also makes the foreign key property optional unless it's configured otherwise.

级联删除Cascade delete

您可以使用熟知的 API 显式配置给定关系的级联删除行为。You can use the Fluent API to configure the cascade delete behavior for a given relationship explicitly.

有关每个选项的详细讨论,请参阅级联删除See Cascade Delete for a detailed discussion of each option.

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

其他关系模式Other relationship patterns

一对一One-to-one

一对多关系在两侧都有一个引用导航属性。One to one relationships have a reference navigation property on both sides. 它们遵循与一对多关系相同的约定,但在外键属性上引入了唯一索引,以确保只有一个依赖项与每个主体相关。They follow the same conventions as one-to-many relationships, but a unique index is introduced on the foreign key property to ensure only one dependent is related to each principal.

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 会根据其检测外键属性的能力,选择其中一个实体作为依赖项。EF will choose one of the entities to be the dependent based on its ability to detect a foreign key property. 如果选择了错误的实体作为依赖项,则可以使用熟知的 API 来更正此问题。If the wrong entity is chosen as the dependent, you can use the Fluent API to correct this.

使用 "流畅" API 配置关系时,请使用 HasOneWithOne 方法。When configuring the relationship with the Fluent API, you use the HasOne and WithOne methods.

配置外键时,需要指定依赖实体类型-请注意以下列表中提供的泛型参数 HasForeignKeyWhen configuring the foreign key you need to specify the dependent entity type - notice the generic parameter provided to HasForeignKey in the listing below. 在一对多关系中,可以清楚地表明具有引用导航的实体是依赖项,并且具有集合的实体是主体。In a one-to-many relationship it is clear that the entity with the reference navigation is the dependent and the one with the collection is the principal. 但这并不是一对一的关系,因此需要显式定义它。But this is not so in a one-to-one relationship - hence the need to explicitly define it.

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

多对多Many-to-many

目前尚不支持多对多关系,没有实体类来表示联接表。Many-to-many relationships without an entity class to represent the join table are not yet supported. 但是,您可以通过包含联接表的实体类并映射两个不同的一对多关系,来表示多对多关系。However, you can represent a many-to-many relationship by including an entity class for the join table and mapping two separate one-to-many relationships.

class MyContext : DbContext
{
    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 int PostId { get; set; }
    public Post Post { get; set; }

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