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

  1. User (Id, Name, Email) + UserProfile (Id, UserId, Bio, AvatarUrl, CreatedAt).
  2. Конфігурація через Fluent API.
  3. Створіть користувача з профілем, завантажте через Include.

Завдання 1.2: Many-to-Many (Implicit)

  1. Student (Id, Name) ↔ Course (Id, Title) — implicit join.
  2. Додайте студентів на курси.
  3. Запит: всі курси конкретного студента, всі студенти на курсі.

Рівень 2: Практичний

Завдання 2.1: Many-to-Many (Explicit)

  1. Додайте Enrollment (StudentId, CourseId, EnrollmentDate, Grade).
  2. Конфігурація з складовим PK.
  3. Запит: середня оцінка студента, студенти з оцінкою > 90 на курсі.

Завдання 2.2: Self-Referencing

  1. Employee (Id, Name, ManagerId, Manager, Subordinates).
  2. Побудуйте дерево організації.
  3. Запит: всі підлеглі конкретного менеджера (через Include).

Рівень 3: Архітектура

Завдання 3.1: Повна модель бібліотеки

Об'єднайте:

  1. AuthorBook (M:N через BookAuthor з Role).
  2. BookTag (M:N implicit).
  3. BookBookDetail (1:1).
  4. Category → self-referencing.
  5. Запит: книги автора 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. Для дерев та ієрархій.
Copyright © 2026