Как реализуется связь many-to-many в реляционных базах данных?

«Как реализуется связь many-to-many в реляционных базах данных?» — вопрос из категории Базы данных, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Связь «многие-ко-многим» реализуется через промежуточную таблицу (junction/join table), которая содержит внешние ключи на обе связанные таблицы. Я использовал этот подход в проектах с MySQL и PostgreSQL.

Базовый пример SQL:

-- Основные таблицы
CREATE TABLE authors (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE books (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL,
    isbn VARCHAR(13) UNIQUE,
    published_year INT
);

-- Промежуточная таблица
CREATE TABLE author_book (
    author_id INT NOT NULL,
    book_id INT NOT NULL,
    -- Дополнительные поля, если нужно
    contribution_type ENUM('main', 'co-author', 'editor') DEFAULT 'main',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (author_id, book_id),
    FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
    INDEX idx_book_id (book_id) -- Для обратных запросов
);

Запросы для работы со связью:

-- Найти все книги автора
SELECT b.* 
FROM books b
JOIN author_book ab ON b.id = ab.book_id
WHERE ab.author_id = 1;

-- Найти всех авторов книги
SELECT a.*
FROM authors a
JOIN author_book ab ON a.id = ab.author_id
WHERE ab.book_id = 5;

-- Добавить связь
INSERT INTO author_book (author_id, book_id, contribution_type) 
VALUES (1, 5, 'main');

-- Удалить связь
DELETE FROM author_book 
WHERE author_id = 1 AND book_id = 5;

Реализация в ORM:

// Laravel Eloquent
class Author extends Model {
    public function books() {
        return $this->belongsToMany(Book::class, 'author_book')
                    ->withPivot('contribution_type')
                    ->withTimestamps();
    }
}

class Book extends Model {
    public function authors() {
        return $this->belongsToMany(Author::class, 'author_book')
                    ->withPivot('contribution_type');
    }
}

// Использование
$author = Author::find(1);
$author->books()->attach(5, ['contribution_type' => 'main']);
$books = $author->books()->where('published_year', '>', 2000)->get();

Ключевые моменты:

  • Составной первичный ключ (author_id, book_id) гарантирует уникальность связи
  • ON DELETE CASCADE автоматически удаляет связи при удалении записей
  • Промежуточная таблица может содержать дополнительные атрибуты связи (метаданные)
  • Индексы на обоих внешних ключах критически важны для производительности
  • В сложных случаях можно использовать триггеры для поддержания целостности