Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
Еще одним способом ассоциации объектов является связь многие-ко-многим (many-to-many). Примером подобного отношения может служить посещение студентами университетских курсов. Один студент может посещать сразу несколько курсов, и, в свою очередь, один курс может посещаться множеством студентов.
Начиная с версии Entity Framework Core 5.0 во фреймворке появилась возможность автоматической генерации связи многие ко многим. И вначале рассмотрим подобную возможность, а потом посмотрим что было до версии 5.0.
Например, определим следующие классы моделей и контекста:
public class Course { public int Id { get; set; } public string Name { get; set; } public List<Student> Students { get; set; }= new List<Student>(); } public class Student { public int Id { get; set; } public string Name { get; set; } public List<Course> Courses { get; set; } = new List<Course>(); } public class ApplicationContext : DbContext { public DbSet<Course> Courses { get; set; } public DbSet<Student> Students { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } }
В данном случае все студенты, поступившие на курс, будут помещаться в свойство Students класса Course. Аналогично, все курсы студента будут храниться в свойстве Courses класса Student. То есть стандартная связь многие ко многие. Однако, при создании базы данных в ней будет три таблицы:
CREATE TABLE [dbo].[Courses] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Courses] PRIMARY KEY CLUSTERED ([Id] ASC) ); CREATE TABLE [dbo].[Students] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Students] PRIMARY KEY CLUSTERED ([Id] ASC) ); CREATE TABLE [dbo].[CourseStudent] ( [CoursesId] INT NOT NULL, [StudentsId] INT NOT NULL, CONSTRAINT [PK_CourseStudent] PRIMARY KEY CLUSTERED ([CoursesId] ASC, [StudentsId] ASC), CONSTRAINT [FK_CourseStudent_Courses_CoursesId] FOREIGN KEY ([CoursesId]) REFERENCES [dbo].[Courses] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_CourseStudent_Students_StudentsId] FOREIGN KEY ([StudentsId]) REFERENCES [dbo].[Students] ([Id]) ON DELETE CASCADE );
То есть в реальности на уровне базы данных создается промежуточная таблица, которая хранит связи между студентами и курсами. Тем не менее на уровне кода C# нам не надо создавать промежуточную сущность, Entity Framework Core начиная с версии 5.0 умеет управлять подобной связью.
Рассмотрим, как мы можем работать с моделями в связи многие ко многим. Добавление:
using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); // создание и добавление моделей Student tom = new Student { Name = "Tom" }; Student alice = new Student { Name = "Alice" }; Student bob = new Student { Name = "Bob" }; db.Students.AddRange(tom, alice, bob); Course algorithms = new Course { Name = "Алгоритмы" }; Course basics = new Course { Name = "Основы программирования" }; db.Courses.AddRange(algorithms, basics); // добавляем к студентам курсы tom.Courses.Add(algorithms); tom.Courses.Add(basics); alice.Courses.Add(algorithms); bob.Courses.Add(basics); db.SaveChanges(); }
Стоит отметить, что здесь мы добавляем курсы к студентам, но также можем сделать и наоборот - добавить студентов к курсам:
algorithms.Students.AddRange(new List<Student>() { tom, bob });
Вывод данных:
using (ApplicationContext db = new ApplicationContext()) { var courses = db.Courses.Include(c => c.Students).ToList(); // выводим все курсы foreach (var c in courses) { Console.WriteLine($"Course: {c.Name}"); // выводим всех студентов для данного кура foreach (Student s in c.Students) Console.WriteLine($"Name: {s.Name}"); Console.WriteLine("-------------------"); } }
Консольный вывод:
Course: Алгоритмы Name: Tom Name: Alice ------------------------------- Course: Основы программирования Name: Tom Name: Bob -------------------------------
Обновление данных (например, удалим у студента один курс и добавим другой):
using (ApplicationContext db = new ApplicationContext()) { Student alice = db.Students.Include(s => s.Courses).FirstOrDefault(s => s.Name == "Alice"); Course algorithms = db.Courses.FirstOrDefault(c => c.Name == "Алгоритмы"); Course basics = db.Courses.FirstOrDefault(c => c.Name == "Основы программирования"); if (alice != null && algorithms != null && basics != null) { // удаление курса у студента alice.Courses.Remove(algorithms); // добавление нового курса студенту alice.Courses.Add(basics); db.SaveChanges(); } }
Удаление же студента или курса из базы данных приведет к тому, что все строки из промежуточной таблицы, которые связаны с удаляемым объектом, также будут удалены:
Student student = db.Students.FirstOrDefault(); db.Students.Remove(student); db.SaveChanges();
EF Core 5 позволяет сконфигурировать отношение многие ко многие. Как правило, подобная конфигурация требуется для настройки промежуточной таблицы. Например, мы хотим переопределить название таблицы, которая создается для промежуточной сущности.
public class ApplicationContext : DbContext { public DbSet<Course> Courses { get; set; } public DbSet<Student> Students { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Course>() .HasMany(c => c.Students) .WithMany(s => s.Courses) .UsingEntity(j => j.ToTable("Enrollments")); } }
Последний метод в цепочке - UsingEntity позволяет настроить промежуточную таблицу. Фактически объект, представленный буквой j как раз представляет условную промежуточную сущность, для которой создается таблица. Так, в данном случае промежуточная таблица будет называться "Enrollments".
EF Core 5 автоматически создает промежуточную таблицу с двумя столбцами, через которые она связана с двумя другими таблицами. Однако иногда может потребоваться добавить в промежуточную таблицу еще какие-то данные. Например, в случае со студентами и курсами мы бы могли хранить в промежуточной таблице также дату поступления студента на выбранный курс. В этом случае на уровне кода C# лучше создать помежуточную сущность, которая будет содержать описание данных, которые мы хотим определить. Например:
using System; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; namespace HelloApp { public class Course { public int Id { get; set; } public string Name { get; set; } public List<Student> Students { get; set; }= new List<Student>(); public List<Enrollment> Enrollments { get; set; } = new List<Enrollment>(); } public class Student { public int Id { get; set; } public string Name { get; set; } public List<Course> Courses { get; set; } = new List<Course>(); public List<Enrollment> Enrollments { get; set; } = new List<Enrollment>(); } public class Enrollment { public int StudentId { get; set; } public Student Student { get; set; } public int CourseId { get; set; } public Course Course { get; set; } public int Mark { get; set; } // оценка студента public DateTime EnrollmentDate { get; set; } // дата зачисления } public class ApplicationContext : DbContext { public DbSet<Course> Courses { get; set; } public DbSet<Student> Students { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Course>() .HasMany(c => c.Students) .WithMany(s => s.Courses) .UsingEntity<Enrollment>( j => j .HasOne(pt => pt.Student) .WithMany(t => t.Enrollments) .HasForeignKey(pt => pt.StudentId), j => j .HasOne(pt => pt.Course) .WithMany(p => p.Enrollments) .HasForeignKey(pt => pt.CourseId), j => { j.Property(pt => pt.EnrollmentDate).HasDefaultValueSql("CURRENT_TIMESTAMP"); j.Property(pt => pt.Mark).HasDefaultValue(3); j.HasKey(t => new { t.CourseId, t.StudentId }); j.ToTable("Enrollments"); }); } } }
Для определения данных, которые будут храниться в промежуточной таблице, здесь определена промежуточная сущность Enrollment, которая содержит навигационные свойства на сущности Student и Course, а также содержит два дополнительных свойства EnrollmentDate (дата зачисления на курс) и Mark (оценка студента).
Для конфигурации промежуточной сущности Enrollment также используется метод UsingEntity>Enrollment<()
.
Вначале настраиваются внешние ключи таблиц:
j => j .HasOne(pt => pt.Student) .WithMany(t => t.Enrollments) .HasForeignKey(pt => pt.StudentId), // связь с таблицей Students через StudentId j => j .HasOne(pt => pt.Course) .WithMany(p => p.Enrollments) .HasForeignKey(pt => pt.CourseId), // связь с таблицей Courses через CourseId
В последней части настраиваем свойства сущности Enrollment, а также имя соответствующей таблицы и ее ключи:
j => { j.Property(pt => pt.EnrollmentDate).HasDefaultValueSql("CURRENT_TIMESTAMP"); j.Property(pt => pt.Mark).HasDefaultValue(3); j.HasKey(t => new { t.CourseId, t.StudentId }); j.ToTable("Enrollments"); });
Здесь для конфигурации таблицы и столбцов мы можем применять все ранее рассмотренные методы, например, HasDefaultValueSql
использует
встроенную в T-SQL функцию CURRENT_TIMESTAMP
для установки значения по умолчанию. Также для столбца Mark будет устанавливаться
значение по умолчанию - число 3.
На уровне базы данных для промежуточной сущности будет создаваться следующая таблица:
CREATE TABLE [dbo].[Enrollments] ( [StudentId] INT NOT NULL, [CourseId] INT NOT NULL, [Mark] INT DEFAULT ((3)) NOT NULL, [EnrollmentDate] DATETIME2 (7) DEFAULT (getdate()) NOT NULL, CONSTRAINT [PK_Enrollments] PRIMARY KEY CLUSTERED ([CourseId] ASC, [StudentId] ASC), CONSTRAINT [FK_Enrollments_Courses_CourseId] FOREIGN KEY ([CourseId]) REFERENCES [dbo].[Courses] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_Enrollments_Students_StudentId] FOREIGN KEY ([StudentId]) REFERENCES [dbo].[Students] ([Id]) ON DELETE CASCADE );
Теперь рассмотрим некоторые операции. Добавление:
using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); // создание и добавление моделей Student tom = new Student { Name = "Tom" }; Student alice = new Student { Name = "Alice" }; Student bob = new Student { Name = "Bob" }; db.Students.AddRange(tom, alice, bob); Course algorithms = new Course { Name = "Алгоритмы" }; Course basics = new Course { Name = "Основы программирования" }; db.Courses.AddRange(algorithms, basics); // добавляем к студентам курсы tom.Enrollments.Add(new Enrollment { Course = algorithms, EnrollmentDate = DateTime.Now }); tom.Courses.Add(basics); alice.Enrollments.Add(new Enrollment { Course = algorithms, EnrollmentDate = DateTime.Now, Mark = 4 }); bob.Enrollments.Add(new Enrollment { Course = basics, EnrollmentDate = DateTime.Now }); db.SaveChanges(); }
Особенностью работы с данными в данном случае будет то, что мы можем использовать два способа для установки связи одной сущности с другой:
tom.Enrollments.Add(new Enrollment { Course = algorithms, EnrollmentDate = DateTime.Now }); tom.Courses.Add(basics);
В первом случае устанавливаем связь студента с курсом через добавление объекта Enrollment, который обеспечивает эту связь. Во втором случае добавляем курс напрямую в коллекцию курсов студента, что в сою очередь также автоматически добавить данные в связующую таблицу Enrollments.
Вывод данных:
using (ApplicationContext db = new ApplicationContext()) { var courses = db.Courses.Include(c => c.Students).ToList(); // выводим все курсы foreach (var c in courses) { Console.WriteLine($"Course: {c.Name}"); // выводим всех студентов для данного кура foreach (Student s in c.Students) Console.WriteLine($"Name: {s.Name}"); Console.WriteLine("-------------------"); } }
Консольный вывод:
Course: Алгоритмы Name: Tom Name: Alice ------------------------------- Course: Основы программирования Name: Tom Name: Bob -------------------------------
Либо задействуем промежуточную сущность:
using (ApplicationContext db = new ApplicationContext()) { var courses = db.Courses.Include(c => c.Students).ToList(); // выводим все курсы foreach (var c in courses) { Console.WriteLine($"Course: {c.Name}"); // выводим всех студентов для данного кура foreach (var s in c.Enrollments) Console.WriteLine($"Name: {s.Student.Name} Date: {s.EnrollmentDate.ToShortDateString()} Mark: {s.Mark}"); Console.WriteLine("-------------------"); } }
Консольный вывод:
Course: Алгоритмы Name: Tom Date: 17.11.2000 Mark: 3 Name: Alice Date: 17.11.2000 Mark: 4 ------------------------------- Course: Основы программирования Name: Tom Date: 17.11.2000 Mark: 3 Name: Bob Date: 17.11.2000 Mark: 3 -------------------------------
Также рассмотрим, как можно работать со связью многие ко многим в EntityFramework Core до версии 5.0.
public class ApplicationContext : DbContext { public DbSet<Course> Courses { get; set; } public DbSet<Student> Students { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<StudentCourse>() .HasKey(t => new { t.StudentId, t.CourseId }); modelBuilder.Entity<StudentCourse>() .HasOne(sc => sc.Student) .WithMany(s => s.StudentCourses) .HasForeignKey(sc => sc.StudentId); modelBuilder.Entity<StudentCourse>() .HasOne(sc => sc.Course) .WithMany(c => c.StudentCourses) .HasForeignKey(sc => sc.CourseId); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } } public class Course { public int Id { get; set; } public string Name { get; set; } public List<StudentCourse> StudentCourses { get; set; } public Course() { StudentCourses = new List<StudentCourse>(); } } public class Student { public int Id { get; set; } public string Name { get; set; } public List<StudentCourse> StudentCourses { get; set; } public Student() { StudentCourses = new List<StudentCourse>(); } } public class StudentCourse { public int StudentId { get; set; } public Student Student { get; set; } public int CourseId { get; set; } public Course Course { get; set; } public System.DateTime EnrollmentDate { get; set; } }
Фактически связь многие-ко-многим здесь разбивается на две связи один-ко-многим: один Student - много StudentCourse, один Course - много StudentCourse. Класс StudentCourse в этом случае выполняет роль связующей сущности.
В итоге в создаваемой базе данных будет три таблицы:
CREATE TABLE [dbo].[Courses] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Courses] PRIMARY KEY CLUSTERED ([Id] ASC) ); CREATE TABLE [dbo].[Students] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Students] PRIMARY KEY CLUSTERED ([Id] ASC) ); CREATE TABLE [dbo].[StudentCourse] ( [StudentId] INT NOT NULL, [CourseId] INT NOT NULL, [EnrollmentDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_StudentCourse] PRIMARY KEY CLUSTERED ([StudentId] ASC, [CourseId] ASC), CONSTRAINT [FK_StudentCourse_Courses_CourseId] FOREIGN KEY ([CourseId]) REFERENCES [dbo].[Courses] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_StudentCourse_Students_StudentId] FOREIGN KEY ([StudentId]) REFERENCES [dbo].[Students] ([Id]) ON DELETE CASCADE );
Используем модели. Добавление данных:
using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Student s1 = new Student { Name = "Tom"}; Student s2 = new Student { Name = "Alice" }; Student s3 = new Student { Name = "Bob" }; db.Students.AddRange(s1, s2, s3); Course c1 = new Course { Name = "Алгоритмы" }; Course c2 = new Course { Name = "Основы программирования" }; db.Courses.AddRange(c1, c2); // добавляем к студентам курсы s1.StudentCourses.Add(new StudentCourse { Course = c1, Student = s1, EnrollmentDate = DateTime.Now }); s2.StudentCourses.Add(new StudentCourse { Course = c1, Student = s2, EnrollmentDate = DateTime.Now }); s2.StudentCourses.Add(new StudentCourse { Course = c2, Student = s2, EnrollmentDate = DateTime.Now }); db.SaveChanges(); }
Получение данных:
using (ApplicationContext db = new ApplicationContext()) { var courses = db.Courses.Include(c => c.StudentCourses) .ThenInclude(sc => sc.Student) .ToList(); // выводим все курсы foreach (var c in courses) { Console.WriteLine($"\nCourse: {c.Name}"); // выводим всех студентов для данного кура foreach (StudentCourse sc in c.StudentCourses) Console.WriteLine($"Name: {sc.Student?.Name} Date: {sc.EnrollmentDate.ToShortDateString()}"); Console.WriteLine("-------------------"); // для красоты } }
Консольный вывод:
Course: Алгоритмы Name: Tom Date: 17.11.2020 Name: Alice Date: 17.11.2020 ------------------------------- Course: Основы программирования Name: Alice Date: 17.11.2020 -------------------------------
Редактирование:
// удаление курса у студента Student student = db.Students.Include(s=>s.StudentCourses).FirstOrDefault(s => s.Name == "Alice"); Course course = db.Courses.FirstOrDefault(c => c.Name == "Алгоритмы"); if (student != null && course != null) { var studentCourse = student.StudentCourses.FirstOrDefault(sc => sc.CourseId == course.Id); student.StudentCourses.Remove(studentCourse); db.SaveChanges(); }