Зачем нужны ограничения внешнего ключа
Что проверяют
Понимаете ли вы, какие гарантии даёт база и что потеряет приложение, если перенести эти проверки в код на Go. Следом часто спрашивают, как сервис обрабатывает ошибку нарушения ключа и почему между микросервисами внешних ключей не бывает.
Как отвечать
- ссылочная целостность: база не даст вставить заказ с
user_id, которого нет вusers, и не даст удалить пользователя с заказами. Проверка в Go черезSELECTпередINSERTэтого не гарантирует: между запросами другая транзакция успеет удалить строку; - поведение при удалении:
ON DELETEзадаётNO ACTION(по умолчанию),RESTRICT,CASCADE,SET NULLилиSET DEFAULT.CASCADEудобен для позиций заказа, но опасен на центральных таблицах; - отложенная проверка:
DEFERRABLE INITIALLY DEFERREDпереносит проверку на коммит, это нужно для циклических связей и массовой загрузки; - ошибки в Go: в PostgreSQL нарушение ключа приходит с кодом
23503. С pgx его достают черезerrors.As(err, &pgErr)и сравниваютpgErr.Codeсpgerrcode.ForeignKeyViolation. Такую ошибку переводят в понятный ответ API, например 422, а не в 500.
Цена тоже есть: вставка в дочернюю таблицу читает родительскую строку и ставит на неё блокировку FOR KEY SHARE, а удаление родителя проверяет дочерние записи. На горячих таблицах это заметно, но отказываются от внешних ключей только осознанно.
Где спотыкаются
Уверены, что внешний ключ сам создаёт индекс. У родителя индекс есть всегда, ссылаться можно только на первичный или уникальный ключ, а user_id в дочерней таблице PostgreSQL не индексирует. Без ручного индекса удаление пользователя сканирует всю таблицу заказов. MySQL с InnoDB такой индекс создаёт сам.
Добавляют ограничение на большую живую таблицу одной командой. Правильнее ADD CONSTRAINT ... NOT VALID, а затем отдельно VALIDATE CONSTRAINT, который не блокирует запись.
Пытаются связать внешним ключом таблицы разных сервисов. Между базами это невозможно, там целостность держат событиями и проверками на уровне API.