В системе заказов набор допустимых статусов должен изменяться без миграции схемы. Какое ограничение лучше и...

В системе заказов набор допустимых статусов должен изменяться без миграции схемы. Какое ограничение лучше использовать, чтобы база не принимала неизвестные статусы?

Проходите собеседования с ИИ помощником Hintsage

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

Используйте справочную таблицу статусов и внешний ключ из заказов на её уникальный идентификатор. Тогда допустимость значения контролируется самой базой, а добавление нового статуса выполняется добавлением строки, без изменения определения таблицы заказов.

Исторический контекст

Свободный текст в атрибутах вроде статуса приводит к расхождениям: например, одновременно появляются значения «оплачен», «Оплачен» и «paid». Ограничение CHECK решает проблему фиксированного набора значений, но любое изменение списка требует изменения схемы или текста ограничения.

Связь со справочником отделяет структуру данных от изменяемого набора допустимых значений. Такой подход широко применяется для доменов, которыми нужно управлять как данными: статусов, типов документов, валют и категорий.

Постановка проблемы

Если статус хранится строкой без ограничения, приложение может записать опечатку или значение, которого не существует в бизнес-модели. Проверка только в приложении ненадёжна: данные могут поступить из другого сервиса, административного скрипта или пакетной загрузки.

Фиксированный CHECK лучше защищает целостность, но плохо подходит для часто изменяемого справочника. Кроме того, удаление статуса, который уже используется заказами, может нарушить историческую интерпретацию данных.

Подробное решение

Создайте таблицу-справочник с уникальным ключом и ссылайтесь на него из таблицы заказов:

CREATE TABLE order_status ( status_id INTEGER PRIMARY KEY, status_code VARCHAR(30) NOT NULL UNIQUE, is_active BOOLEAN NOT NULL ); CREATE TABLE orders ( order_id INTEGER PRIMARY KEY, status_id INTEGER NOT NULL, FOREIGN KEY (status_id) REFERENCES order_status(status_id) );

Внешний ключ разрешает в заказе только такой status_id, для которого существует строка в order_status. Уникальность status_code не даёт создать два справочных значения с одним бизнес-кодом.

Обычно используйте стабильный идентификатор или код как ссылочную цель, а отображаемое название храните отдельно. Если бизнес допускает изменение названия статуса, идентификатор не меняется, поэтому ссылки заказов остаются корректными.

Для уже использованных статусов безопаснее применять мягкое отключение через is_active, а не физическое удаление. При этом внешний ключ гарантирует существование статуса, но сам по себе не запрещает выбор неактивного статуса; такое правило нужно дополнительно обеспечивать процессом, триггером или другой моделью данных.

Внешний ключ проверяет существование значения, но не порядок переходов: например, он не запрещает переход из «доставлен» обратно в «создан». Переходы между статусами — отдельное бизнес-правило, которое можно моделировать таблицей разрешённых переходов или контролировать на уровне транзакционной логики.

Компромисс решения — дополнительное соединение со справочником при чтении и необходимость определить политику изменения и удаления справочных строк. Взамен база получает единый источник допустимых значений и защиту от ссылок на несуществующий статус.

Ситуация из практики

В интернет-магазине статусы сначала хранились как строки. После подключения нескольких интеграций появились варианты «shipped», «отправлен» и «Отправлен», из-за чего отчёты считали один и тот же статус по-разному.

Рассматривались два варианта. CHECK со списком значений был простым и быстрым, но каждое изменение статуса требовало миграции схемы. Проверка в приложении не добавляла изменений в базу, однако не защищала от прямых записей и ошибок другого сервиса.

Выбрали справочник статусов с внешним ключом, уникальным кодом и признаком активности. Исторические статусы не удаляли, а деактивировали; результатом стали единые значения в отчётах и возможность добавлять новые статусы обычной операцией над данными.

Что кандидаты часто упускают

  1. Почему недостаточно сделать внешний ключ на текстовый код статуса?

Это допустимо, если код в справочнике объявлен уникальным или является первичным ключом. Внешний ключ должен ссылаться на ключ, однозначно идентифицирующий строку; ссылка на неуникальный столбец не определяет единственную родительскую запись и потому не выражает корректную ссылочную целостность.

  1. Можно ли внешним ключом запретить использование неактивных статусов?

Обычный внешний ключ проверяет только наличие родительской строки. Он не учитывает условие вроде is_active = true, поэтому для такого правила нужны дополнительная проверка при изменении заказа, триггер или отдельная модель, в которой активность выражена самой ссылочной структурой.

  1. Что произойдёт при попытке удалить статус, используемый заказами?

При обычном поведении внешнего ключа удаление будет отклонено, поскольку после него появились бы заказы с неразрешённой ссылкой. CASCADE опасен потерей или удалением зависимых заказов, а SET NULL несовместим с обязательным status_id и в любом случае оставляет заказ без статуса. Для исторических данных обычно выбирают запрет удаления или мягкое отключение записи справочника.