Ключи и связи
Что это такое
Здесь решают, что идентифицирует каждую строку и как таблицы ссылаются друг на друга. Первичный ключ задаёт идентичность строки на всё время её существования, внешний ключ — ссылка, которую движок умеет поддерживать, а ограничение уникальности фиксирует, какие естественные значения не должны повторяться. Вместе они образуют скелет схемы: большинство поздних проблем либо вырастает из них, либо ими же и отсекается.
Почему это важно
Выбор ключа фактически необратим. Смена первичного ключа после того, как таблица выросла, означает перезапись и самой таблицы, и каждой ссылки на неё, обычно с простоем. Естественный ключ вроде адреса электронной почты или номера заказа выглядит удобным, пока значение не придётся изменить; однако тогда изменение распространяется на каждую таблицу, где оно хранилось. Эти же решения определяют и то, поддерживаются ли связи базой данных или лишь подразумеваются приложением.
Ключевые идеи
Сгенерированный идентификатор — устойчивая идентичность
Произвольное значение, не несущее смысла, нельзя обесценить изменением в бизнесе, поэтому ссылки на него остаются действительными. Целые числа компактны и упорядочены; случайные идентификаторы избегают коллизий между системами и скрывают число записей, но стоят размера и локальности. Оба варианта защитимы, а изменяемое бизнес-значение в роли первичного ключа — нет.
id bigint generated -> компактный, последовательный
public_id uuid generated -> непрозрачный, безопасен для показа
email text unique -> ограничивайте уникальность, не идентифицируйте
В интерфейс выносите безопасный идентификатор
Последовательное целое удобно как первичный ключ внутри системы, но плохо в URL: оно выдаёт объём и провоцирует перебор. Отдельный непрозрачный публичный идентификатор держит внутренний ключ вне интерфейса и позволяет менять его, не задевая клиентов. Небольшое дополнение к проекту, а снимает целую категорию забот.
У каждой связи есть форма, и форма решает схему
В связи «один ко многим» внешний ключ ставится на сторону «многих». Связь «многие ко многим» требует собственной таблицы: её первичный ключ обычно и есть пара ссылок, и она же даёт естественное место для атрибутов самой связи. Распознать, какая форма перед вами, — большая часть работы по моделированию.
один клиент -> много заказов orders.customer_id
много заказов <-> много товаров order_items(order_id, product_id, quantity)
Поддержите связь, затем решите, что значит удаление
Объявленный внешний ключ не даёт ссылке указывать на несуществующую строку. То же объявление говорит и о том, что происходит при удалении ссылаемой строки: отказать, удалить зависимые или очистить ссылку. За каждым вариантом стоит свой смысл — строка заказа без заказа бессмысленна, а клиент без заказов осмыслен, — и выбрать это при объявлении яснее, чем обрабатывать в коде.
Естественная уникальность — в ограничении, а не в проверке
Адрес электронной почты должен быть уникальным, и сказать об этом нужно ограничением уникальности, потому что только оно действует при конкурентном доступе. Два потока приложения, которые сначала проверяют, а потом оба вставляют, дадут дубликат, как бы тщательна ни была проверка. Ограничение превращает гонку в ошибку, которую приложение может обработать.
Составные ключи полезны и легко ошибочны
Таблицу, выражающую связь «многие ко многим», естественно идентифицирует пара ссылок, и эта же пара предотвращает дубликаты строк. Порядок столбцов в этом ключе и в любом индексе по нему решает, какие поиски быстры, поэтому ключ работает сразу и как идентичность, и как путь доступа. Там, где эти цели конфликтуют, стоит добавить отдельный идентификатор и проиндексировать пару явно.
Практика
Выберите ключи для трёх таблиц
Спроектируйте ключи для клиента, заказа и строки заказа. Для каждого решите, сгенерирована идентичность или естественна, и нужен ли отдельный публичный идентификатор.
Готово, когда: вы можете назвать значение, которое когда-либо изменится, и подтвердить, что ни одна ссылка от него не зависит.
Выразите связь «многие ко многим»
Смоделируйте товары и теги так, чтобы у товара было много тегов, а у тега много товаров и при этом не заводилось повторяющихся пар.
Готово, когда: вставка одной и той же пары товара и тега дважды отклоняется базой, а тег без товаров всё ещё может существовать.
Частые ошибки
- Изменяемое бизнес-значение в роли первичного ключа — при изменении ломается каждая ссылка; исправить: используйте сгенерированный идентификатор, а бизнес-значение ограничьте уникальностью.
- Последовательный ключ в публичном интерфейсе — объём раскрыт, записи можно перебрать; исправить: используйте отдельный непрозрачный идентификатор.
- Хранение связи «многие ко многим» списком в одной строке — запросы и ограничения становятся неудобными; исправить: добавьте таблицу-связку.
- Проверка уникальности только в коде приложения — конкурентные вставки дают дубликаты; исправить: объявите ограничение уникальности.
- Связи оставлены неподдерживаемыми — копятся строки-сироты; исправить: объявите внешние ключи и осознанно выберите поведение при удалении.
Что читать дальше
- Проектирование баз данных (05-backend-data/14-database-design) — дисциплина, частью которой являются эти решения, поэтому читайте её первой.
- Проектирование под выполняемые запросы (05-backend-data/14-database-design/02-designing-for-the-queries-you-run) — как эти ключи и связи становятся быстрыми путями доступа.
- Таблицы и связи (05-backend-data/07-sql-relational-databases/01-tables-and-relationships) — возможности движка, которые используют эти решения.