> Как описать схему базы данных для модели библиотеки с сущностями автор, книга, читатель и связями между ними (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

Схема библиотеки описывается через три основные сущности: авторы, книги и читатели. Связи: многие-ко-многим между авторами и книгами (через junction table), многие-ко-многим между книгами и читателями (через таблицу выдач с датами). В Go это отражается в структурах с foreign key и слайсами для связей. Основной trade-off - выбор между нормализацией и простотой запросов, а также решение о хранении истории выдач.

Подробное объяснение

Для модели библиотеки стандартная схема включает:

  • authors - id, name, bio
  • books - id, title, year, isbn
  • book_authors - junction table: book_id, author_id (many-to-many)
  • readers - id, name, email
  • loans - id, book_id, reader_id, loan_date, return_date (nullable)

Связь "книга-автор" - классический many-to-many, потому что одна книга может иметь несколько авторов, и один автор пишет много книг. Связь "книга-читатель" - тоже many-to-many, но с дополнительной семантикой: loan_date и return_date делают её исторической. Если нужна только текущая выдача, можно хранить book_id в readers как active_loan, но это нарушает нормализацию и теряет историю.

В Go структуры выглядят так:

GO
type Author struct {
ID int64
Name string
Books []Book // через join
}
type Book struct {
ID int64
Title string
Authors []Author
}
type Reader struct {
ID int64
Name string
Loans []Loan
}
type Loan struct {
ID int64
BookID int64
ReaderID int64
LoanDate time.Time
ReturnDate *time.Time
}

Ключевой момент - индексы. Для loans обязателен индекс по reader_id и book_id, иначе запросы "какие книги у читателя" будут full scan. Для book_authors - составной индекс (book_id, author_id).

На практике

При реализации на Go с PostgreSQL:

  1. Используйте sqlx или pgx для работы с БД.
  2. Миграции - через golang-migrate или goose.
  3. Для many-to-many запросов используйте JOIN или отдельные запросы с последующей сборкой в Go (N+1 проблема решается через IN или JOIN с агрегацией).
  4. Для loans решите: хранить только активные или всю историю. Если историю - добавьте return_date и фильтруйте WHERE return_date IS NULL для активных.
  5. Подумайте о soft delete для книг, если библиотека списывает экземпляры.

Пример запроса "все книги автора":

SQL
SELECT b.* FROM books b
JOIN book_authors ba ON ba.book_id = b.id
WHERE ba.author_id = $1;

Как отвечать на собеседовании

Начните с трёх таблиц и двух junction tables. Объясните, почему many-to-many, а не foreign key напрямую. Затем перейдите к loans - здесь важно показать понимание разницы между текущим состоянием и историей. Упомяните индексы и типичные запросы. Если спросят про нормализацию - скажите, что схема в 3NF, но для частых запросов можно добавить денормализованные поля (например, book_count у автора) при доказанной необходимости. В конце спросите про требования: нужна ли история выдач, сколько данных, какие запросы критичны.

Что проверяет интервьюер

  • Понимание отношений между сущностями (1:1, 1:N, M:N).
  • Умение проектировать junction tables.
  • Осознание trade-off между нормализацией и производительностью.
  • Знание, как это ложится на Go-структуры и SQL-запросы.
  • Способность обсуждать индексы и типичные проблемы (N+1, full scan).

Типичные ошибки

  • Прямой foreign key book_id в readers - теряется история, нарушается many-to-many.
  • Отсутствие junction table для авторов - если книга с двумя авторами, придётся дублировать строки.
  • Не учитывать nullable return_date - без него нельзя отличить активную выдачу от завершённой.
  • Забывать про индексы на внешние ключи - на больших данных запросы деградируют.
  • Хранить даты как строки - в Go и PostgreSQL это должно быть time.Time и timestamp.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью