Конвенции для выявления взаимосвязей

EF Core использует набор соглашений при обнаружении и создании модели на основе классов типов сущностей. В этом документе приведены соглашения, используемые для обнаружения и настройки связей между типами сущностей.

Это важно

Соглашения, описанные здесь, можно переопределить путем явной настройки связи с помощью атрибутов сопоставления или API построения модели.

Подсказка

Приведенный ниже код можно найти в RelationshipConventions.cs.

Исследование навигации

Обнаружение связей начинается с обнаружения навигаций между типами сущностей.

Навигации по справочным материалам

Свойство типа сущности обнаруживается как ссылочная навигация, когда:

  • Свойство является общедоступным.
  • Свойство имеет геттер и сеттер.
  • Тип свойства является или может быть типом сущности. Это означает, что тип
  • Свойство не является статическим.
  • Свойство не является свойством индексатора.

Например, рассмотрим следующие типы сущностей:

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.Author и Author.Blog обнаруживаются в качестве ссылочных навигаций. С другой стороны, следующие свойства не обнаруживаются в качестве ссылочных навигаций:

  • Blog.Id, так как int является сопоставленным примитивным типом
  • Blog.Title, так как "string" является сопоставленным примитивным типом
  • Blog.Uri, так как Uri автоматически преобразуется в сопоставленный примитивный тип
  • Blog.ConsoleKeyInfo, так как ConsoleKeyInfo является типом значения C#
  • Blog.DefaultAuthor, поскольку у свойства нет сеттера
  • Author.Id, так как Guid является сопоставленным примитивным типом
  • Author.Name, так как "string" является сопоставленным примитивным типом
  • Author.BlogId, так как int является сопоставленным примитивным типом

Навигации по коллекции

Свойство типа сущности определяется как коллекционная навигация когда:

  • Свойство является общедоступным.
  • Свойство имеет геттер. Навигации по коллекции могут иметь методы задания, но это не обязательно.
  • Тип свойства представляет собой или реализует IEnumerable<TEntity>, где TEntity является или может быть типом сущности. Это означает, что тип TEntity:
  • Свойство не является статическим.
  • Свойство не является свойством индексатора.

Например, в следующем коде Blog.Tags и Tag.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 и Post обнаруживается путем сопоставления навигаций Blog.Posts и Post.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; }
}

Обнаружена единая взаимосвязь "один к одному" между Blog и Author, выявленная путем связывания навигаций Blog.Author и Author.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; }
}

Одно отношение "многие ко многим" между Post и Tag обнаруживается через связывание навигаций Post.Tags и Tag.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>. Таким образом, в этом примере свойства внешнего ключа являются PostsId и TagsId.
    • Для однонаправленной связи "многие ко многим" свойство внешнего ключа без связанной справочной информации называется <principal entity type name><principal key name>.
  • Свойства внешнего ключа не допускают значения null, что делает обе связи обязательными для объекта соединения.
    • Соглашения об каскадных удалениях означают, что эти связи будут настроены для каскадного удаления.
  • Тип сущности соединения сконфигурирован с составным первичным ключом, состоящим из двух свойств внешнего ключа. Таким образом, в этом примере первичный ключ состоит из PostsId и TagsId.

Это приводит к следующей модели 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> становится списком имен свойств внешнего ключа, разделенным подчеркиваниями.

Дополнительные ресурсы