Ef Core
10.7. Відношення — Один-до-Одного та Багато-до-Багатьох
10.7. Відношення — Один-до-Одного та Багато-до-Багатьох
Вступ
У попередній статті ми розглянули найпоширеніший тип зв'язку — One-to-Many. Тепер розглянемо два інших типи: One-to-One (один-до-одного) та Many-to-Many (багато-до-багатьох).
Передумови: 10.6. Відношення 1:N.
One-to-One (1:1)
Коли використовувати
Зв'язок 1:1 використовується, коли:
- Розділення великої таблиці — рідко використовані стовпці виносяться в окрему таблицю
- Безпека/ізоляція — конфіденційні дані окремо
- Опціональні дані — не кожна книга має детальний опис
Модель
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Author { get; set; } = "";
public int Year { get; set; }
// Один-до-одного: деталі книги (опціональні)
public BookDetail? Detail { get; set; }
}
public class BookDetail
{
public int Id { get; set; }
public string? Summary { get; set; }
public int PageCount { get; set; }
public string? CoverImageUrl { get; set; }
public string? TableOfContents { get; set; }
// Foreign Key + Navigation
public int BookId { get; set; }
public Book Book { get; set; } = null!;
}
Конфігурація
public class BookDetailConfiguration : IEntityTypeConfiguration<BookDetail>
{
public void Configure(EntityTypeBuilder<BookDetail> builder)
{
builder.HasOne(d => d.Book) // BookDetail має одну Book
.WithOne(b => b.Detail) // Book має один BookDetail
.HasForeignKey<BookDetail>(d => d.BookId); // FK у BookDetail
}
}
Використання
using var context = new LibraryContext();
// Створення книги з деталями
var book = new Book
{
Title = "Чистий код",
Author = "Роберт Мартін",
Year = 2008,
Detail = new BookDetail
{
Summary = "Книга про написання чистого, підтримуваного коду.",
PageCount = 464,
CoverImageUrl = "/images/clean-code.jpg"
}
};
context.Books.Add(book);
context.SaveChanges();
// Запит з Include
var bookWithDetail = context.Books
.Include(b => b.Detail)
.FirstOrDefault(b => b.Id == book.Id);
Console.WriteLine($"{bookWithDetail?.Title}: {bookWithDetail?.Detail?.PageCount} сторінок");
Many-to-Many (M:N)
Implicit Join Table (EF Core 5+)
Починаючи з EF Core 5, M:N зв'язки конфігуруються без явної join-сутності:
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
// Many-to-Many: книга може мати кілька тегів
public List<Tag> Tags { get; set; } = new();
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; } = "";
// Many-to-Many: тег може бути у кількох книг
public List<Book> Books { get; set; } = new();
}
EF Core автоматично створює join-таблицю BookTag:
-- Автоматично згенерована join-таблиця
CREATE TABLE BookTag (
BooksId INT NOT NULL,
TagsId INT NOT NULL,
PRIMARY KEY (BooksId, TagsId),
FOREIGN KEY (BooksId) REFERENCES Books(Id) ON DELETE CASCADE,
FOREIGN KEY (TagsId) REFERENCES Tags(Id) ON DELETE CASCADE
);
Конфігурація join-таблиці
builder.HasMany(b => b.Tags)
.WithMany(t => t.Books)
.UsingEntity(j => j.ToTable("BookTags")); // Кастомне ім'я таблиці
Explicit Join Entity (для додаткових полів)
Якщо join-таблиця містить додаткові поля (дата додавання, порядок, рейтинг), потрібна явна сутність:
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public List<BookAuthor> BookAuthors { get; set; } = new();
}
public class Author
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<BookAuthor> BookAuthors { get; set; } = new();
}
// Явна join-сутність з додатковими полями
public class BookAuthor
{
public int BookId { get; set; }
public Book Book { get; set; } = null!;
public int AuthorId { get; set; }
public Author Author { get; set; } = null!;
// Додаткові поля:
public int Order { get; set; } // Порядок автора (основний, співавтор)
public string Role { get; set; } = ""; // "Author", "Editor", "Translator"
}
Конфігурація:
public class BookAuthorConfiguration : IEntityTypeConfiguration<BookAuthor>
{
public void Configure(EntityTypeBuilder<BookAuthor> builder)
{
// Складовий Primary Key
builder.HasKey(ba => new { ba.BookId, ba.AuthorId });
builder.HasOne(ba => ba.Book)
.WithMany(b => b.BookAuthors)
.HasForeignKey(ba => ba.BookId);
builder.HasOne(ba => ba.Author)
.WithMany(a => a.BookAuthors)
.HasForeignKey(ba => ba.AuthorId);
builder.Property(ba => ba.Role)
.HasMaxLength(50)
.HasDefaultValue("Author");
}
}
Використання M:N
using var context = new LibraryContext();
// Implicit M:N (Tags)
var book = new Book { Title = "DDD" };
var tag1 = new Tag { Name = "Architecture" };
var tag2 = new Tag { Name = "Design Patterns" };
book.Tags.Add(tag1);
book.Tags.Add(tag2);
context.Books.Add(book);
context.SaveChanges();
// Запит
var booksWithTags = context.Books
.Include(b => b.Tags)
.ToList();
foreach (var b in booksWithTags)
Console.WriteLine($"{b.Title}: {string.Join(", ", b.Tags.Select(t => t.Name))}");
// Explicit M:N (BookAuthors)
var author1 = new Author { Name = "Kent Beck" };
var author2 = new Author { Name = "Martin Fowler" };
var collab = new Book { Title = "Planning Extreme Programming" };
context.Add(collab);
context.AddRange(author1, author2);
context.SaveChanges();
context.Set<BookAuthor>().AddRange(
new BookAuthor { BookId = collab.Id, AuthorId = author1.Id, Order = 1, Role = "Author" },
new BookAuthor { BookId = collab.Id, AuthorId = author2.Id, Order = 2, Role = "Co-Author" }
);
context.SaveChanges();
Self-Referencing Relationships
Сутність посилається на саму себе — типовий приклад: категорії з підкатегоріями:
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = "";
// Батьківська категорія (nullable — корені немають батька)
public int? ParentId { get; set; }
public Category? Parent { get; set; }
// Дочірні категорії
public List<Category> Children { get; set; } = new();
}
Конфігурація:
builder.HasOne(c => c.Parent)
.WithMany(c => c.Children)
.HasForeignKey(c => c.ParentId)
.OnDelete(DeleteBehavior.Restrict); // Не можна видалити батька з дітьми
Використання:
using var context = new LibraryContext();
var root = new Category
{
Name = "Програмування",
Children = new List<Category>
{
new()
{
Name = "C#",
Children = new List<Category>
{
new() { Name = "EF Core" },
new() { Name = "ASP.NET Core" }
}
},
new() { Name = "Java" },
new() { Name = "Python" }
}
};
context.Categories.Add(root);
context.SaveChanges();
// Запит: дерево категорій
var tree = context.Categories
.Include(c => c.Children)
.ThenInclude(c => c.Children)
.Where(c => c.ParentId == null) // Тільки кореневі
.ToList();
Практичні завдання
Рівень 1: Базовий
Завдання 1.1: One-to-One
User(Id, Name, Email) +UserProfile(Id, UserId, Bio, AvatarUrl, CreatedAt).- Конфігурація через Fluent API.
- Створіть користувача з профілем, завантажте через Include.
Завдання 1.2: Many-to-Many (Implicit)
Student(Id, Name) ↔Course(Id, Title) — implicit join.- Додайте студентів на курси.
- Запит: всі курси конкретного студента, всі студенти на курсі.
Рівень 2: Практичний
Завдання 2.1: Many-to-Many (Explicit)
- Додайте
Enrollment(StudentId, CourseId, EnrollmentDate, Grade). - Конфігурація з складовим PK.
- Запит: середня оцінка студента, студенти з оцінкою > 90 на курсі.
Завдання 2.2: Self-Referencing
Employee(Id, Name, ManagerId, Manager, Subordinates).- Побудуйте дерево організації.
- Запит: всі підлеглі конкретного менеджера (через Include).
Рівень 3: Архітектура
Завдання 3.1: Повна модель бібліотеки
Об'єднайте:
Author↔Book(M:N черезBookAuthorз Role).Book↔Tag(M:N implicit).Book→BookDetail(1:1).Category→ self-referencing.- Запит: книги автора X з тегами Y в категорії Z.
Резюме
One-to-One
HasOne().WithOne(). FK в залежній таблиці. Для опціональних або конфіденційних даних.
Many-to-Many (Implicit)
Два List навігації. EF Core 5+ автоматично створює join-таблицю.
Many-to-Many (Explicit)
Явна join-сутність з додатковими полями. Складовий PK, два HasOne().WithMany().
Self-Referencing
Сутність посилається сама на себе. ParentId? + Parent + Children. Для дерев та ієрархій.