CHECK-ограничение — это документация с зубами.
Вот и весь урок. Но давайте посмотрим, как он усваивается не во время чтения, а во время выполнения.
Что произошло
Мы используем ARIA — автономную CRM и систему автоматизации в Elevare Digital. Новые контакты автоматически попадают в последовательности действий — без участия человека.
В какой-то момент новые записи перестали регистрироваться. Родительская операция (создание контакта) полностью отменялась. Ни последовательности, ни контакта. И никакой полезной ошибки для вызывающего кода.
Виновником оказалась колонка region с CHECK-ограничением:
ALTER TABLE enrollment_tracker ADD CONSTRAINT region_allowed CHECK (region IN ('north', 'south', 'east', 'west', 'central'));Таблица была создана для одной конкретной программы, охватывающей пять регионов. Позже код начал использовать её как общий трекер регистрации, передавая значения, которых ограничение не ожидало. PostgreSQL отклонил вставку. А поскольку вставка происходила внутри триггера, вся родительская транзакция откатилась.
Колонка называлась region. В названии нет намёка на то, что допустимы только пять значений. Ограничение это говорило. Но никто его не прочитал.
Почему триггер усугубляет ситуацию
Если вставлять напрямую в таблицу с нарушением CHECK, вы получите ошибку сразу. Раздражает, но локализовано.
Когда вставка происходит внутри триггера на другой таблице, ошибка всплывает и прерывает операцию, вызвавшую триггер. Вызывающий код видит, что его запись не удалась. Он может даже не знать о существовании триггера, не говоря уже о том, какое ограничение сработало внутри.
-- Триггер срабатывает при INSERT в contacts CREATE OR REPLACE FUNCTION enroll_contact() RETURNS trigger AS $$ BEGIN -- Этот INSERT может подорвать родительский INSERT INTO contacts INSERT INTO enrollment_tracker (contact_id, region, enrolled_at) VALUES (NEW.id, NEW.region, now()); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_enroll AFTER INSERT ON contacts FOR EACH ROW EXECUTE FUNCTION enroll_contact();Теперь попробуйте это:
INSERT INTO contacts (id, name, region) VALUES (gen_random_uuid(), 'Acme Corp', 'southeast'); -- ERROR: new row for relation "enrollment_tracker" violates -- check constraint "region_allowed" -- DETAIL: Failing row contains (..., southeast, ...). -- Контакт НЕ создан.southeast — вполне допустимая бизнес-сущность. Просто таблица о ней не знала.
Исправление: проверяйте до вставки
Поняв ограничение, мы легко его исправили. Проверяем допустимые значения в коде (или в самом триггере) перед попыткой вставки. Если значение не разрешено, пропускаем запись о регистрации и логируем. Родительская операция завершается успешно.
CREATE OR REPLACE FUNCTION enroll_contact() RETURNS trigger AS $$ DECLARE allowed_regions TEXT[] := ARRAY['north', 'south', 'east', 'west', 'central']; BEGIN IF NEW.region = ANY(allowed_regions) THEN INSERT INTO enrollment_tracker (contact_id, region, enrolled_at) VALUES (NEW.id, NEW.region, now()); ELSE -- Логируем, не прерывая родительскую запись INSERT INTO enrollment_skipped (contact_id, region, skipped_at, reason) VALUES (NEW.id, NEW.region, now(), 'region not in enrollment_tracker allowed list'); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql;Альтернативно, можно динамически получать список из определения ограничения — это удобно, если значения могут расширяться:
SELECT consrc FROM pg_constraint WHERE conname = 'region_allowed' AND conrelid = 'enrollment_tracker'::regclass;Вернёт что-то вроде (region = ANY (ARRAY['north'::text, 'south'::text, ...])). Не самый чистый парсинг, но точно показывает, что задумано схемой.
Как читать унаследованные ограничения
Перед записью в любую таблицу, которую вы не создавали:
-- psql shortcut \d enrollment_tracker -- Или прямой запрос SELECT tc.constraint_name, tc.constraint_type, cc.check_clause FROM information_schema.table_constraints tc LEFT JOIN information_schema.check_constraints cc ON tc.constraint_name = cc.constraint_name WHERE tc.table_name = 'enrollment_tracker';Так вы найдёте ограничения:
- CHECK — допустимые значения, диапазоны, межколоночные правила
- UNIQUE — уникальность, которую вы могли не предполагать
- NOT NULL — обязательные колонки
- FOREIGN KEY — ссылочные зависимости
Всё это не скрыто. Просто редко читается.
Настоящий урок
Таблица называлась enrollment_tracker. Звучит универсально. Но она была создана для конкретной программы с пятью регионами, и CHECK-ограничение было единственным местом, где эта область действия была зафиксирована.
Когда поздний код начал использовать её как универсальный трекер, он привнёс предположение, о котором не знал. Схема проявила это предположение в момент записи, внутри триггера, и это уничтожило родительскую запись.
Ограничения схемы — это самая близкая к обязательной документация, которая есть у большинства баз данных. Они не устаревают. Их не забывают в вики. Они принудительно выполняются.
Читайте их до записи. А не после.
— Майк Кларк, основатель Elevare Digital.
Комментарии (0)
Войдите, чтобы комментировать.
Пока нет комментариев. Будьте первым.