Поделиться через


Соглашения об обнаружении связей

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

Важно!

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

Совет

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

Обнаружение навигаций

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

Справочные навигации

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

  • Свойство является общедоступным.
  • Свойство имеет метод получения и набор.
    • Метод установки не должен быть общедоступным; он может быть частным или иметь другие специальные возможности.
    • Метод задания может быть только init-only.
  • Тип свойства имеет тип сущности или может быть типом сущности. Это означает, что тип
    • Должен быть ссылочным типом.
    • Не должно быть явно настроено как тип примитивного свойства.
    • Не следует сопоставлять с типом примитивного свойства, используемым поставщиком базы данных.
    • Не должно быть автоматически преобразовано в тип примитивного свойства, сопоставленного поставщиком базы данных.
  • Свойство не является статическим.
  • Свойство не является свойством индексатора.

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

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

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