关系发现约定

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

重要

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

小窍门

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

发现导航工具

关系发现始于识别实体类型之间的导航

参考导航

如果发现实体类型的属性作为 引用导航时:

  • 该属性为公共的。
  • 该属性具有 getter 和 setter。
    • setter 不需要是公共的,它可以是私有的,也可以具有任何其他 访问级别
    • setter 可以是 仅限初始化
  • 属性类型是或可能是实体类型。 这意味着类型
    • 必须是 引用类型
    • 不得显式设置为 基元属性
    • 不能由所使用的数据库提供程序映射为原始属性类型。
    • 不能 自动转换为 由所使用的数据库提供程序映射的基元属性类型。
  • 该属性不是静态的。
  • 该属性不是 索引器属性

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

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。 集合导航可以具有 setter,但这不是必需的。
  • 属性类型是 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。如果找到此类反函数,则两个导航将组合在一起,形成单个双向关系。

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

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

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

通过将BlogPost导航进行配对发现Blog.PostsPost.Blog之间的单对多关系:

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

BlogAuthor 之间,通过配对 Blog.AuthorAuthor.Blog 导航发现了一对一关系:

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

通过配对PostTag导航,可以发现Post.TagsTag.Posts之间的一对多对多关系。

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 将创建一个 阴影属性 来表示外键。 阴影属性:

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

级联删除

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

多对多

多对多关系 没有主体和从属关系,两端均不包含外键属性。 相反,多对多关系使用连接实体类型,该类型包含外键对,这些外键对分别指向多对多关系的两端。 请考虑以下实体类型,根据约定可发现多对多关系:

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 Sever 上:

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> 将成为外键属性名称的下划线分隔列表。

其他资源