关系发现的约定

EF Core 在发现和生成基于实体类型类的模型时使用一组约定。 本文档总结了用于发现和配置实体类型之间的关系的约定。

重要

可以通过使用映射属性或模型生成 API 显式配置关系来重写此处所述的约定。

提示

可以在 RelationshipConventions.cs 中找到以下代码。

发现导航

关系发现首先发现实体类型之间的导航

引用导航

在以下情况下,实体类型的属性会被发现为引用导航

  • 该属性是公共的。
  • 该属性具有 Getter 和 Setter。
    • Setter 不需要是公共的;它可以是专用的或具有任何其他辅助功能
    • Setter 可以是仅限 Init
  • 属性类型也可能是实体类型。 这意味着类型
    • 必须是引用类型
    • 不得显式配置为基元属性类型
    • 不得被使用的数据库提供程序映射为基元属性类型。
    • 不得自动转换为使用的数据库提供程序映射的基元属性类型。
  • 属性不是静态的。
  • 属性不是索引器属性

例如,请考虑以下实体类型:

public class Blog
{
    // Not discovered as reference navigations:
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public Uri? Uri { get; set; }
    public ConsoleKeyInfo ConsoleKeyInfo { get; set; }
    public Author DefaultAuthor => new() { Name = $"Author of the blog {Title}" };

    // Discovered as a reference navigation:
    public Author? Author { get; private set; }
}

public class Author
{
    // Not discovered as reference navigations:
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public int BlogId { get; set; }

    // Discovered as a reference navigation:
    public Blog Blog { get; init; } = null!;
}

对于这些类型,Blog.AuthorAuthor.Blog 被发现为引用导航。 另一方面,以下属性不会被发现为引用导航:

  • Blog.Id,因为 int 是映射的基元类型
  • Blog.Title,因为“string”是映射的基元类型
  • Blog.Uri,因为 Uri 会自动转换为映射的基元类型
  • Blog.ConsoleKeyInfo,因为 ConsoleKeyInfo 是 C# 值类型
  • Blog.DefaultAuthor,因为该属性没有 Setter
  • Author.Id,因为 Guid 是映射的基元类型
  • Author.Name,因为“string”是映射的基元类型
  • Author.BlogId,因为 int 是映射的基元类型

集合导航

在以下情况下,实体类型的属性会被发现为集合导航

  • 该属性是公共的。
  • 该属性具有一个 Getter。 集合导航可以具有资源库,但这不是必需的。
  • 属性类型实现 IEnumerable<TEntity>,其中 TEntity 也可能是实体类型。 这意味着类型为 TEntity
    • 必须是引用类型
    • 不得显式配置为基元属性类型
    • 不得被使用的数据库提供程序映射为基元属性类型。
    • 不得自动转换为使用的数据库提供程序映射的基元属性类型。
  • 属性不是静态的。
  • 属性不是索引器属性

例如,在以下代码中,Blog.TagsTag.Blogs 被发现为集合导航:

public class Blog
{
    public int Id { get; set; }
    public List<Tag> Tags { get; set; } = null!;
}

public class Tag
{
    public Guid Id { get; set; }
    public IEnumerable<Blog> Blogs { get; } = new List<Blog>();
}

配对导航

例如,一旦发现从实体类型 A 到实体类型 B 的导航,接下来必须确定此导航是否按相反方向进行(即从实体 B 到实体类型 A)。如果发现这种反向导航,则会将两个导航配对在一起,形成单个双向关系。

关系类型取决于导航及其反向导航是引用导航还是集合导航。 具体而言:

  • 如果一个导航是集合导航,另一个导航是引用导航,则关系为一对多
  • 如果两个导航都是引用导航,则关系是一对一
  • 如果两个导航都是集合导航,则关系是多对多

以下示例中显示了其中每种类型的关系的发现:

通过配对 Blog.PostsPost.Blog 导航会在 BlogPost 之间发现单个一对多关系:

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

通过配对 Blog.AuthorAuthor.Blog 导航会在 BlogAuthor 之间发现单个一对一关系:

public class Blog
{
    public int Id { get; set; }
    public Author? Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

通过配对 Post.TagsTag.Posts 导航会在 PostTag 之间发现单个多对多关系:

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

注意

如果两个导航表示两个不同的单向关系,则此导航配对可能不正确。 在这种情况下,必须显式配置这两个关系。

仅当两种类型之间存在单个关系时,关系配对才有效。 必须显式配置两种类型之间的多个关系。

注意

此处的说明是两种不同类型之间的关系。 但是,同一类型可能位于关系的两端,因此单个类型具有两个相互配对的导航。 这称为自引用关系。

发现外键属性

显式发现或配置关系的导航后,这些导航将用于发现关系的适当外键属性。 在以下情况下,属性被发现为外键:

  • 属性类型与主体实体类型上的主键或备用键兼容。
    • 如果类型相同,或者如果外键属性类型是主键或备用键属性类型的可为空版本,则这些类型是兼容的。
  • 属性名称与外键属性的命名约定之一匹配。 命名约定为:
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • 此外,如果已使用模型生成 API 显式配置依赖端,并且依赖主键兼容,则依赖主键也将用作外键。

提示

“Id”后缀可以包含任何大小写。

以下实体类型显示了其中每个命名约定的示例。

Post.TheBlogKey 被发现为外键,因为它与模式 <navigation property name><principal key property name> 匹配:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.TheBlogID 被发现为外键,因为它与模式 <navigation property name>Id 匹配:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogID { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.BlogKey 被发现为外键,因为它与模式 <principal entity type name><principal key property name> 匹配:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.Blogid 被发现为外键,因为它与模式 <principal entity type name>Id 匹配:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? Blogid { get; set; }
    public Blog? TheBlog { get; set; }
}

注意

对于一对多导航,外键属性必须位于具有引用导航的类型上,因为这是依赖实体。 对于一对一关系,外键属性的发现用于确定哪种类型表示关系的依赖端。 如果未发现外键属性,则必须使用 HasForeignKey 配置依赖端。 有关此示例,请参阅一对一关系

上述规则也适用于复合外键,其中复合的每个属性都必须具有与主键或备用键的相应属性兼容的类型,并且每个属性名称必须与上述命名约定之一匹配。

确定基数

EF 使用发现的导航和外键属性来确定关系的基数及其主体端和依赖端:

  • 如果有一个不成对的引用导航,则关系配置为单向一对多,引用导航位于依赖端。
  • 如果有一个不成对的集合导航,则关系配置为单向一对多,集合导航位于主体端。
  • 如果有已配对的引用导航和集合导航,则关系配置为双向一对多,集合导航位于主体端。
  • 如果引用导航与其他引用导航配对,则:
    • 如果在一端发现了外键属性,但在另一端未发现外键属性,则关系配置为双向一对一,外键属性位于依赖端。
    • 否则,无法确定依赖端,EF 会引发异常,指示必须显式配置依赖端。
  • 如果集合导航与另一个集合导航配对,则关系配置为双向多对多

阴影外键属性

如果 EF 已确定关系的依赖端,但未发现外键属性,则 EF 将创建一个阴影属性来表示外键。 阴影属性:

  • 在关系的主体端具有主键或备用键属性的类型。
    • 默认情况下,该类型可为空,使关系默认为可选。
  • 如果依赖端有导航,则将使用与主键或备用键属性名称串接的导航名称来命名阴影外键属性。
  • 如果依赖端没有导航,则将使用与主键或备用键属性名称串接的主体实体类型名称来命名阴影外键属性。

级联删除

根据约定,所需的关系配置为级联删除。 可选关系配置为不级联删除。

多对多

多对多关系没有主体端和依赖端,两端均不包含外键属性。 相反,多对多关系使用联接实体类型,其中包含指向多对多任一端的外键对。 请考虑以下实体类型,根据约定发现多对多关系:

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

此发现中使用的约定为:

  • 联接实体类型名为 <left entity type name><right entity type name>。 因此,在此示例中为 PostTag
    • 联接表与联接实体类型同名。
  • 为联接实体类型提供了关系的每个方向的外键属性。 这些命名为 <navigation name><principal key name>。 因此,在此示例中,外键属性为 PostsIdTagsId
    • 对于单向多对多,没有关联导航的外键属性名为 <principal entity type name><principal key name>
  • 外键属性不可为空,使之与联接实体的两种关系为必选关系。
    • 级联删除约定意味着将为级联删除配置这些关系。
  • 联接实体类型配置了由两个外键属性组成的复合主键。 因此,在此示例中,主键由 PostsIdTagsId 组成。

这将产生以下 EF 模型:

Model:
  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Tags (ICollection<Tag>) CollectionTag Inverse: Posts
    Keys:
      Id PK
  EntityType: Tag
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Posts (ICollection<Post>) CollectionPost Inverse: Tags
    Keys:
      Id PK
  EntityType: PostTag (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      PostsId (no field, int) Indexer Required PK FK AfterSave:Throw
      TagsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      PostsId, TagsId PK
    Foreign keys:
      PostTag (Dictionary<string, object>) {'PostsId'} -> Post {'Id'} Cascade
      PostTag (Dictionary<string, object>) {'TagsId'} -> Tag {'Id'} Cascade
    Indexes:
      TagsId

使用 SQLite 时,会转换为以下数据库架构:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tag" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tag_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tag" ("Id") ON DELETE CASCADE);

CREATE INDEX "IX_PostTag_TagsId" ON "PostTag" ("TagsId");

索引

根据约定,EF 为外键的一个或多个属性创建数据库索引。 创建的索引类型取决于:

  • 关系的基数
  • 关系是可选的还是必需的
  • 构成外键的属性数

对于一对多关系,根据约定创建简单的索引。 为可选关系和必需关系创建相同的索引。 例如,在 SQLite 上:

CREATE INDEX "IX_Post_BlogId" ON "Post" ("BlogId");

或在 SQL Server 上:

CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

对于必需的一对一关系,将创建唯一索引。 例如,在 SQLite 上:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

或在 SQL Server 上:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]);

对于可选的一对一关系,在 SQLite 上创建的索引是相同的:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

但是,在 SQL Server 上时,会添加 IS NOT NULL 筛选器以更好地处理 null 外键值。 例如:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;

对于复合外键,将创建涵盖所有外键列的索引。 例如:

CREATE INDEX "IX_Post_ContainingBlogId1_ContainingBlogId2" ON "Post" ("ContainingBlogId1", "ContainingBlogId2");

注意

EF 不会为现有索引或主键约束已涵盖的属性创建索引。

如何停止 EF 为外键创建索引

索引有开销,并且按照此处的要求,可能并不总是适合为所有 FK 列创建索引。 为此,可以在生成模型时删除 ForeignKeyIndexConvention

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

如果需要,仍可为需要索引的外键列显式创建索引。

外键约束名称

根据约定,外键约束名为 FK_<dependent type name>_<principal type name>_<foreign key property name>。 对于组合外键,<foreign key property name> 将成为外键属性名称的下划线分隔列表。

其他资源