Entity Framework Core – организация связей между моделями: соглашения, аннотации данных и Fluent API

Внимание

Данный материал является частью цикла статей «Основы Entity Framework Core». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Начало работы с Entity Framework Core в ASP.NET Core - модели, DbContext, конфигурация
  2. Настройка нереляционных свойств в EF Core
  3. Миграции в Entity Framework Core
  4. Entity Framework Core – организация связей между моделями: соглашения, аннотации данных и Fluent API
  5. Запросы к базе данных в Entity Framework Core
  6. Изменение данных с помощью Entity Framework Core

Взаимосвязи EF Core - концепции и свойства навигации

В настоящее время у нас есть только один класс сущности (модели), класс Student, но довольно скоро мы собираемся создать остальную часть модели базы данных в нашем приложении. Но прежде чем мы это сделаем, очень важно понять некоторые основные концепции при работе с реляционными базами данных и моделями.

Когда мы создаем связь между двумя объектами, одна из них становится основной сущностью, а другая - зависимой сущностью. Основная сущность - это основная сущность во взаимоотношениях. Он содержит первичный ключ как свойство, на которое зависимая сущность ссылается через внешний ключ. Зависимая сущность, с другой стороны, - это сущность, которая содержит внешний ключ, который относится к первичному ключу основной сущности.

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

Кроме того, давайте объясним отношения Required и Optional в EF Core. Обязательная связь - это связь, в которой внешний ключ не может быть нулевым. Это означает, что должен существовать главный объект. Необязательное отношение - это отношение, в котором внешний ключ может иметь значение NULL и, следовательно, основной объект может отсутствовать.

Настройка One-to-One связи

Отношение "один к одному" означает, что строка в одной таблице может относиться только к одной строке в другой таблице в связи. Это не такая распространенная связь, потому что она обычно обрабатывается как «все данные в одной таблице», но иногда (когда мы хотим разделить наши сущности) полезно разделить данные на две таблицы.

Самый простой способ настроить этот тип отношений - использовать подход по соглашению, и это именно то, что мы собираемся сделать. Итак, давайте сначала создадим еще один класс в проекте Entities с именем StudentDetails:

public class StudentDetails
{
    [Column("StudentDetailsId")]
    public Guid Id { get; set; }
    public string Address { get; set; }
    public string AdditionalInformation { get; set; }
}

Теперь, чтобы установить связь между классами Student и StudentDetails, нам нужно добавить свойство навигации по ссылкам с обеих сторон. Итак, давайте сначала изменим класс Student:

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }

    public StudentDetails StudentDetails { get; set; }
}

И давайте изменим класс StudentDetails:

public class StudentDetails
{
    [Column("StudentDetailsId")]
    public Guid Id { get; set; }
    public string Address { get; set; }
    public string AdditionalInformation { get; set; }

    public Guid StudentId { get; set; }
    public Student Student { get; set; }
}

Мы можем видеть, что класс Student имеет свойство навигации по ссылке к классу StudentDetails, а класс StudentDetails имеет внешний ключ и свойство навигации Student.

В результате мы можем создать новую миграцию и применить ее:

PM> Add-Migration OneToOneRelationshipStudent_StudentDetails
PM> Update-Database

Вот результат:

Настройка One-to-One связи EF Core

Отлично, отлично работает.

Дополнительные пояснения

Как мы объяснили в первой статье, EF Core ищет все общедоступные свойства DbSet<T> в классе DbContext для создания таблиц в базе данных. Затем он ищет все общедоступные свойства в классе T для сопоставления столбцов. Но он также выполняет поиск всех общедоступных свойств навигации в классе T и создает дополнительные таблицы и столбцы, связанные с типом свойства навигации. Итак, в нашем примере в классе Student EF Core находит свойство навигации StudentDetails и создает дополнительную таблицу со своими столбцами.

Конфигурация отношений One-to-Many

В этом разделе мы узнаем, как создавать отношения "один ко многим" всеми тремя способами. Итак, прежде чем мы начнем, давайте создадим дополнительный класс модели Evaluation в проекте Entities:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }
}

Использование условного подхода для создания отношений «один ко многим»

Давайте посмотрим на различные соглашения, которые автоматически настраивают связь "один ко многим" между классами Student и Evaluation.

Первый подход включает свойство навигации в основной сущности, классе Student:

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }

    public StudentDetails StudentDetails { get; set; }

    public ICollection<Evaluation> Evaluations { get; set; }
}

В классе ApplicationContext есть свойство DbSet, и, как мы объяснили, EF Core выполняет поиск по классу Student, чтобы найти все свойства навигации для создания соответствующих таблиц в базе данных.

Еще один способ создать связь "один ко многим" - это добавить свойство Student в класс Evaluation без свойства ICollection в классе Student класс:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    public Student Student { get; set; }
}

Чтобы этот подход работал, мы должны добавить свойство DbSet<Evaluation> Evaluations в класс ApplicationContext.

Третий подход по Конвенции заключается в использовании комбинации предыдущих. Итак, мы можем добавить свойство навигации ICollection<Evaluation> Evaluations в класс Student и добавить свойство навигации Student Student в класс Evaluation. Конечно, при таком подходе нам не нужно свойство DbSet<Evaluation> Evaluations в классе ApplicationContext.

Это результат любого из этих трех подходов:

Настройка связей сущностей в EF Core

Мы видим, что связь была создана правильно, но наш внешний ключ является полем, допускающим значение NULL. Это связано с тем, что оба свойства навигации имеют значение по умолчанию null. Это отношение также называется необязательным отношением (мы говорили об этом в первой части этой статьи).

Если мы хотим создать требуемую связь между сущностями Student и Evaluation, мы должны включить внешний ключ в класс Evaluation. :

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Строка должна быть короче 50 символов")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }
    
    public StudentDetails StudentDetails { get; set; }
    
    public ICollection<Evaluation> Evaluations { get; set; }
}

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    public Guid StudentId { get; set; }
    public Student Student { get; set; }
}

Теперь, когда мы выполним нашу миграцию, мы увидим следующий результат:

Миграции EF Core

Очевидно, что наши отношения сейчас необходимы.

Подход с аннотациями данных

Подход с использованием аннотаций к данным содержит только два атрибута, связанных с отношениями. Атрибуты [ForeignKey] и [InverseProperty].

Атрибут [ForeignKey] позволяет нам определять внешний ключ для свойства навигации в классе модели. Итак, давайте изменим класс Evaluation, добавив этот атрибут:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    [ForeignKey(nameof(Student))]
    public Guid StudentId { get; set; }
    public Student Student { get; set; }
} 

Мы применили атрибут [ForeignKey] поверх свойства StudentId (которое является внешним ключом в этом классе), присвоив ему имя свойства навигации Student. Но работает и наоборот:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }
   
    public Guid StudentId { get; set; }
    [ForeignKey(nameof(StudentId))]
    public Student Student { get; set; }
}

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

[ForeignKey («Свойство1», «Свойство2»)].

Какой бы способ мы ни выбрали, результат будет таким же, как и при подходе «по соглашению». Мы собираемся создать требуемую связь между этими двумя таблицами:

Установление связей меду таблицами в EF Core

Подход Fluent API для конфигурации One-to-Many

Чтобы создать отношение «один ко многим» с этим подходом, нам нужно удалить атрибут [ForeignKey] из класса Evaluation и изменить StudentConfiguration, добавив этот код:

builder.HasMany(e => e.Evaluations)
    .WithOne(s => s.Student)
    .HasForeignKey(s => s.StudentId);
С помощью такого кода мы сообщаем EF Core, что наша сущность Student (объект построителя имеет тип) могут быть связаны со многими объектами Evaluation. Мы также заявляем, что Evaluation находится во взаимосвязи только с одной сущностью Student. Наконец, мы предоставляем информацию о внешнем ключе в этой связи.

Результат будет таким же:

One-to-Many связь в EF Core

Здесь нужно упомянуть одну вещь.

Для модели базы данных, такой как мы определили, нам не нужен метод HasForeignKey. Это потому, что свойство внешнего ключа в классе Evaluation имеет тот же тип и то же имя, что и первичный ключ в классе Student. Это означает, что по Конвенции это отношение все равно будет обязательным. Но если бы у нас был внешний ключ с другим именем, например StudId, тогда понадобился бы метод HasForeignKey, потому что в противном случае ядро EF создало бы необязательную связь между классами Evaluation и Student.

Конфигурация отношений Many-to-Many (многие-ко-многим)

Это реализация версии 3.1 EF Core. Это справедливо для EF Core версии 5, но в версии 5 это можно было бы сделать немного иначе. Мы объясним это в следующем разделе.

Прежде чем мы начнем объяснять, как настроить эту связь, давайте создадим необходимые классы в проекте Entities:

public class Subject
{
    [Column("SubjectId")]
    public Guid Id { get; set; }
    public string SubjectName { get; set; }
}

public class StudentSubject
{
    public Guid StudentId { get; set; }
    public Student Student { get; set; }

    public Guid SubjectId { get; set; }
    public Subject Subject { get; set; }
}

Теперь мы можем изменить классы Student и Subject, предоставив свойство навигации для каждого класса по направлению к классу StudentSubject:

public class Subject
{
    [Column("SubjectId")]
    public Guid Id { get; set; }
    public string SubjectName { get; set; }

    public ICollection<StudentSubject> StudentSubjects { get; set; }
}
public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }
        
    public StudentDetails StudentDetails { get; set; }
        
    public ICollection<Evaluation> Evaluations { get; set; }

    public ICollection<StudentSubject> StudentSubjects { get; set; }
}

В Entity Framework Core мы должны создать объединяющуюся сущность для объединяемой таблицы (StudentSubject). Этот класс содержит внешние ключи и свойства навигации из классов Student и Subject. Кроме того, классы Student и Subject имеют свойства навигации ICollection по отношению к классу StudentSubject. Таким образом, отношения «многие ко многим» - это всего лишь два отношения «один ко многим».

Мы создали наши сущности, и теперь нам нужно создать необходимую конфигурацию. Для этого давайте создадим класс StudentSubjectConfiguration в папке Entities/Configuration:

public class StudentSubjectConfiguration : IEntityTypeConfiguration<StudentSubject>
{
    public void Configure(EntityTypeBuilder<StudentSubject> builder)
    {
        builder.HasKey(s => new { s.StudentId, s.SubjectId });

        builder.HasOne(ss => ss.Student)
            .WithMany(s => s.StudentSubjects)
            .HasForeignKey(ss => ss.StudentId);

        builder.HasOne(ss => ss.Subject)
            .WithMany(s => s.StudentSubjects)
            .HasForeignKey(ss => ss.SubjectId);
    }
}

Как мы уже говорили, многие-ко-многим - это всего лишь две взаимосвязи EF Core «один ко многим», и это именно то, что мы настраиваем в нашем коде. Мы создаем первичный ключ для таблицы StudentSubject, который в данном случае является составным ключом. После настройки первичного ключа мы используем знакомый код для создания конфигураций отношений.

Теперь нам нужно изменить метод OnModelBuilder в классе ApplicationContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new StudentConfiguration());
    modelBuilder.ApplyConfiguration(new StudentSubjectConfiguration());
}

После этих изменений мы можем создать миграцию и применить ее:

PM> Add-Migration ManyToManyRelationship

PM> Update-Database

Вот результат:

Связь Many-to-Many в EF Core

Отличная работа. Давай продолжаем.

Примечание по .NET 5

В .NET 5 нам не нужны ни таблица StudentSubject, ни класс StudentSubjectConfiguration. По умолчанию, если наш класс Student имеет свойство навигации для класса Subject, а класс Subject имеет свойство навигации для класса Student, этого вполне достаточно. Никакой дополнительной настройки не требуется.

По сути, класс Student должен иметь public ICollection Subjects {get; set; }, а класс Subject должен иметь public ICollection Students {get; set; } свойство. Нет необходимости ни в третьем классе, ни в свойствах навигации для этого класса.

Но если вы хотите изначально заполнить данные для таблиц Student и Subject и заполнить третью таблицу идентификаторами обоих таблиц, вам придется использовать реализацию, которую мы использовали для версии 3.1.

Метод OnDelete

Метод OnDelete настраивает действия удаления между реляционными сущностями. Мы можем добавить этот метод в конец конфигурации отношений, чтобы решить, как будут выполняться действия удаления.

В методе OnDelete можно использовать следующие значения:

  • Restrict - действие удаления не применяется к зависимым объектам. Это означает, что мы не можем удалить основную сущность, если у нее есть связанная зависимая сущность.
  • SetNull - зависимая сущность не удаляется, но для ее свойства внешнего ключа установлено значение null.
  • ClientSetNull - если EF Core отслеживает зависимую сущность, ее внешний ключ имеет значение null, и эта сущность не удаляется. Если он не отслеживает зависимую сущность, то применяются правила базы данных.
  • Cascade - зависимая сущность удаляется вместе с основной сущностью.

Мы также можем видеть это из кода в нашем файле миграции:

onDelete в миграциях EF Core

Мы можем изменить этот тип поведения, изменив код конфигурации в классе StudentConfiguration:

builder.HasMany(e => e.Evaluations)
     .WithOne(s => s.Student)
     .HasForeignKey(s => s.StudentId)
     .OnDelete(DeleteBehavior.Restrict);

Давайте создадим еще один перенос:

PM> Добавление миграции StudentEvaluationRestrictDelete

И взгляните на сгенерированный код миграции:

Миграции FluentApi с onDelete в EF Core

Заключение

Настройка взаимосвязей EF Core в нашей модели базы данных - очень важная часть процесса моделирования.

Мы увидели, что EF Core предлагает нам несколько способов добиться этого и максимально упростить процесс.

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