Связи: один-ко-многим и многие-ко-многим
Моделируйте и запрашивайте связи «один-ко-многим» и «многие-ко-многим» с помощью свойств навигации и Fluent API.
«Связи: один-ко-многим и многие-ко-многим» — бесплатный урок C# Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Связи в EF Core
EF Core моделирует три типа связей между сущностями: один к одному, один ко многим и многие ко многим. Каждая связь выражается навигационными свойствами и при необходимости настраивается с помощью Fluent API.
Один ко многим: настройка сущностей
У Blog есть много Post. Зависимая сущность (Post) содержит внешний ключ. Навигационные свойства с обеих сторон позволяют перемещаться по связи в любом направлении.
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int BlogId { get; set; } // FK
public Blog Blog { get; set; } = null!; // nav property
}Fluent API: один ко многим
EF Core определяет связь по соглашению, но для большей ясности и настройки каскадного удаления её можно явно задать с помощью Fluent API.
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
}Запросы с Include (жадная загрузка)
Используйте Include, чтобы загрузить связанные данные в рамках того же запроса. Используйте ThenInclude, чтобы перейти глубже по графу навигации.
// Load blog with all its posts
var blog = await _db.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.Id == 1);
// Load posts with their comments too
var blogs = await _db.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();Многие ко многим без сущности-связки
EF Core 5+ поддерживает связи многие ко многим без явно заданной сущности-связки. Он автоматически создаёт таблицу связей, используя имена типов сущностей.
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Course> Courses { get; set; } = new List<Course>();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; } = "";
public ICollection<Student> Students { get; set; } = new List<Student>();
}
// EF creates StudentCourse join table automaticallyМногие ко многим с сущностью-связкой
Если таблице связей нужны дополнительные столбцы, например дата зачисления или оценка, создайте явную сущность-связку с составным первичным ключом.
public class Enrollment
{
public int StudentId { get; set; }
public Student Student { get; set; } = null!;
public int CourseId { get; set; }
public Course Course { get; set; } = null!;
public DateTime EnrolledAt { get; set; }
public decimal? Grade { get; set; }
}
// Fluent API config:
mb.Entity<Enrollment>().HasKey(e => new { e.StudentId, e.CourseId });
mb.Entity<Enrollment>().HasOne(e => e.Student).WithMany(s => s.Enrollments);
mb.Entity<Enrollment>().HasOne(e => e.Course).WithMany(c => c.Enrollments);Добавление связанных данных
Добавьте связанные сущности в коллекцию родительской сущности перед вызовом SaveChangesAsync. EF Core автоматически обработает значения FK.
var blog = new Blog { Name = "Dev Notes" };
blog.Posts.Add(new Post { Title = "Hello EF Core" });
blog.Posts.Add(new Post { Title = "Migrations Deep Dive" });
_db.Blogs.Add(blog);
await _db.SaveChangesAsync();
// Inserts Blog + 2 Posts with correct BlogId FKФильтрация связанных данных
В LINQ можно фильтровать данные по навигационным свойствам. EF Core автоматически преобразует такие запросы в SQL JOIN.
// Blogs that have at least one published post
var popularBlogs = await _db.Blogs
.Where(b => b.Posts.Any(p => p.IsPublished))
.Select(b => new { b.Name, PostCount = b.Posts.Count(p => p.IsPublished) })
.OrderByDescending(b => b.PostCount)
.ToListAsync();Каскадное удаление
При использовании DeleteBehavior.Cascade удаление главной сущности (Blog) автоматически удаляет все зависимые сущности (Post). Другие варианты: SetNull, Restrict, NoAction.
// Delete a blog and all its posts
var blog = await _db.Blogs.FindAsync(1);
_db.Blogs.Remove(blog!);
await _db.SaveChangesAsync();
// Cascade: all Posts with BlogId=1 are also deletedСущности-владельцы для объектов-значений
Типы сущностей-владельцев отлично подходят для объектов-значений, например Address. Они хранятся в той же таблице, что и владелец, и не имеют независимого идентификатора.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string Country { get; set; } = "";
}
// Fluent API:
mb.Entity<Customer>().OwnsOne(c => c.ShippingAddress);Практика: заказ с позициями
Полный шаблон для интернет-магазина: Order — главная сущность со множеством OrderLines — зависимых сущностей, каждая из которых ссылается на Product.
var order = await _db.Orders
.Include(o => o.Lines)
.ThenInclude(l => l.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order is null) return Results.NotFound();
var summary = new
{
OrderId = order.Id,
Total = order.Lines.Sum(l => l.Quantity * l.Product.Price),
Items = order.Lines.Select(l => new
{
l.Product.Name,
l.Quantity,
LineTotal = l.Quantity * l.Product.Price
})
};
return Results.Ok(summary);Быстрая проверка
Когда EF Core автоматически создаёт таблицу связей без явно заданной сущности-связки?
Итоги: связи в EF Core
Главное:
- Один ко многим: FK находится у зависимой сущности, навигационные свойства есть с обеих сторон; используйте Include для загрузки
- Многие ко многим (простой вариант): две навигации ICollection — EF Core автоматически создаёт таблицу связей
- Многие ко многим (с дополнительными данными): явная сущность-связка с составным PK
- Каскадное удаление определяет, что происходит с зависимыми сущностями при удалении главной
- Сущности-владельцы моделируют объекты-значения в той же таблице, что и владелец
Часто задаваемые вопросы
Урок «Связи: один-ко-многим и многие-ко-многим» бесплатный?
Да — полный текст урока «Связи: один-ко-многим и многие-ко-многим» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Связи: один-ко-многим и многие-ко-многим»?
Моделируйте и запрашивайте связи «один-ко-многим» и «многие-ко-многим» с помощью свойств навигации и Fluent API. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Связи: один-ко-многим и многие-ко-многим»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы DbContext и DbSet
- Миграции и управление схемой
- Операции CRUD с EF Core
- Связи: один-ко-многим и многие-ко-многим