Изменчивые перечисления

Изменчивые перечисления помещают список, который меняется вместе с бизнес-данными — категории товаров, регионы, каналы продаж, — в тип enum или ограничение CHECK. Тогда каждому новому значению нужна миграция схемы, удаление значения enum PostgreSQL требует пересоздания типа, а для списка допустимых значений приложение должно изучать схему или дублировать её список в коде. Глава показывает, когда такие списки принадлежат таблице значений, а когда стабильный enum всё ещё лучше. Билл Карвин называет это 31 Flavors.

Также известен как

31 Flavors в оглавлении SQL Antipatterns Билла Карвина. Здесь вредное условие сужено до поддерживаемых бизнесом словарей, жизненный цикл которых не укладывается в выпуски схемы.

Контекст

Отдел продаж открывает и закрывает каналы, меняет подписи и выбирает, какие показывать в формах новых заказов. Сначала есть лишь web и store, поэтому встроенного enum или локального списка CHECK ... IN (...) кажется достаточно. Позднее список становится операционными данными, которыми управляют люди, не развёртывающие изменения схемы.

Проблема

Каждое бизнес-добавление кодируется как DDL, а строки для подписи, доступности и других метаданных нет. Типы enum PostgreSQL имеют порядок объявления и отдельную идентичность типа; существующие элементы нельзя удалить или переставить без пересоздания типа. Это конкретное несоответствие каналам, выводимым из обращения с сохранением смысла старых продаж.

Список CHECK можно заменить, удалив элемент после согласования сохранённых строк, поэтому не называйте удаление невозможным во всех случаях. Оба представления доступны через метаданные схемы. Цена в том, что приложение, поддерживающее варианты выбора, должно понимать представление схемы или синхронизировать с ним второй список.

Почему так делают

Короткий список схемы сразу даёт проверку без дополнительной таблицы. Небольшой словарь под управлением разработчиков вполне может меняться только с выпусками, особенно если каждому новому значению всё равно нужно новое поведение приложения. Стабильные enum остаются полезными. Антипаттерн начинается, когда добавление категории должно быть обычной бизнес-операцией, но всё ещё требует миграции типа.

Последствия

  • ➖ Бизнес-сопровождение наследует требования к разрешениям DDL, расписанию и совместимости.
  • ➖ Скрывать выведенный вариант и сохранять старые ссылки неудобно без метаданных каждого значения.
  • ➖ Порядок объявления enum можно ошибочно принять за редактируемый порядок отображения.
  • ➖ Принятие нового элемента базой не означает, что старый код приложения его понимает.
  • ➖ Повторное использование старого кода с другим смыслом переосмысляет прежние записи при любом представлении.

Признаки

  • Заявка на новый регион или канал требует от разработчика изменения enum.
  • Подписи и флаги активности живут в отдельном словаре приложения с написанием enum в качестве ключа.
  • Удаление означает удаление кода из типа, хотя исторические строки всё ещё его используют.
  • Формирование выпадающих списков читает определения каталога базы при обычных запросах.

Как лучше

Выделите словарю собственную Таблицу значений со стабильным ключом и изменяемыми атрибутами представления. Ссылайтесь на ключ из продаж. Выводите запись из обращения без удаления; различайте существование и доступность для нового назначения. Пример обеспечивает целостность принадлежности и запрос вариантов. Присвоение только активных значений требует дополнительного синхронизированного правила записи из главы о таблице значений.

Альтернативы и выбор

Выбор Связь Когда выбирать
Таблица значений Замена Значениям нужны подписи, вывод из обращения или обычное бизнес-сопровождение
Enum PostgreSQL Вариант реализации стабильного словаря Тип под управлением разработчиков имеет небольшой набор элементов, меняющийся с выпусками
Локальный список CHECK Вариант реализации Небольшое стабильное правило принадлежит одному столбцу и не требует метаданных
Таблица значений с защищённым присвоением Дополнение Старые значения должны читаться, но отклонять новые назначения

Полное сравнение — в Атласе решений. Таблица меняет сопровождение вариантов выбора, а не развёртывание кода, который ветвится по ним.

Владение словарём определяет, будут добавления данными или изменениями схемы
Владение словарём определяет, будут добавления данными или изменениями схемы

Постоянно фиксированное техническое различие может остаться в DDL. Одна потребность в метаданных может оправдать строки, даже если добавления редки.

Пример

Структура

Было: допустимые каналы находятся внутри определения типа.

У имени канала нет независимо поддерживаемой строки метаданных
У имени канала нет независимо поддерживаемой строки метаданных

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

CREATE TYPE channel_kind AS ENUM ('web', 'store');
CREATE TABLE sale_old (sale_id bigint PRIMARY KEY, channel channel_kind NOT NULL);
INSERT INTO sale_old VALUES (1, 'web'), (2, 'store');

Новый согласованный канал отклоняется:

INSERT INTO sale_old VALUES (3, 'partner');

PostgreSQL 18.6 возвращает SQLSTATE 22P02: ERROR: invalid input value for enum channel_kind: "partner". ALTER TYPE ... ADD VALUE расширяет тип, но добавленное внутри транзакции значение нельзя использовать до её фиксации.

Стало: каналы — данные, на которые можно ссылаться.

Продажи сохраняют ссылки, когда канал исчезает из списка новых вариантов
Продажи сохраняют ссылки, когда канал исчезает из списка новых вариантов

Каждой продаже нужен один существующий канал. FK проверяет код, а не is_active.

CREATE TABLE sales_channel (
    code text PRIMARY KEY,
    label text NOT NULL,
    is_active boolean NOT NULL DEFAULT true
);
CREATE TABLE sale (
    sale_id bigint PRIMARY KEY,
    channel_code text NOT NULL,
    CONSTRAINT sale_channel_fk FOREIGN KEY (channel_code) REFERENCES sales_channel (code)
);
CREATE INDEX sale_channel_idx ON sale (channel_code);
BEGIN;
LOCK TABLE sale_old IN SHARE ROW EXCLUSIVE MODE;
INSERT INTO sales_channel (code, label)
SELECT DISTINCT channel::text, initcap(channel::text) FROM sale_old;
INSERT INTO sale SELECT sale_id, channel::text FROM sale_old;
COMMIT;
BEGIN;
INSERT INTO sales_channel (code, label) VALUES ('partner', 'Partner sales');
INSERT INTO sale VALUES (3, 'partner');
COMMIT;
UPDATE sales_channel SET is_active = false WHERE code = 'store';
SELECT code, label FROM sales_channel WHERE is_active ORDER BY code;
SELECT s.sale_id, c.label, c.is_active FROM sale AS s
JOIN sales_channel AS c ON c.code = s.channel_code ORDER BY s.sale_id;

Варианты выбора — partner, web. Чтение продаж сохраняет продажу 2 с подписью Store и is_active = false; ни одна продажа не исчезает из соединения. Новый канал и его первая продажа вставляются в одной транзакции. Полный скрипт проверяет ошибку enum, миграцию и следующие две ошибки принадлежности.

INSERT INTO sale VALUES (4, 'unknown');

Это возвращает SQLSTATE 23503: ERROR: insert or update on table "sale" violates foreign key constraint "sale_channel_fk".

DELETE FROM sales_channel WHERE code = 'store';

Удаление используемого store возвращает SQLSTATE 23503: ERROR: update or delete on table "sales_channel" violates foreign key constraint "sale_channel_fk" on table "sale". Намерением было снять флаг активности.

Поддержка в движках

Сбои и конкурентность

Закешированная форма может отправить код после вывода из обращения. FK примера всё ещё принимает существующий неактивный код, потому что доступность вне его ключа. Если это должно завершаться ошибкой, блокируйте и проверяйте доступность при присвоении, а вывод из обращения включите в тот же протокол; реализацию даёт Таблица значений. Один запрос вариантов не обеспечивает правило.

Вывод из обращения меняет доступность, сохраняя идентичность
Вывод из обращения меняет доступность, сохраняя идентичность

Существующие ссылки переживают вывод из обращения. Реактивация кода уместна лишь для прежнего смысла; новому смыслу нужна новая идентичность.

Миграция

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

Сохраняйте совместимость старых писателей до приёма каналов нового формата
Сохраняйте совместимость старых писателей до приёма каналов нового формата

Новый код, который не представляет старый enum, закрывает путь отката к старому коду. Отложите его приём до ухода старых писателей или до запуска определите отображение совместимости. Сохраняйте исходные данные до конца сверки и не обещайте отсутствие блокировок при замене типа или изменении CHECK; см. правила ALTER TABLE PostgreSQL и Расширение и свёртывание.

Связанные паттерны и антипаттерны

results matching ""

    No results matching ""