Перейти к содержанию

Ограничения целостности

Тип integer позволяет сохранить число минус один, а text — пустое название. Для каталога оба значения технически представимы, но не соответствуют договору: в серии должно быть положительное число глав, а название нужно читателю. Ограничения целостности закрепляют такие правила непосредственно в базе, чтобы разные клиенты не создавали несовместимое состояние.

Используем готовую схему PostgreSQL 17.11 из catalog-db/lesson-05 в архиве серии. При восстановлении она уже содержит ограничения; далее объясним их смысл. Ошибочные записи приводятся как примеры для разбора, в исполняемый lesson.sql они не включены. SQL пока не запускался, поэтому сообщения об ошибках описываются ожидаемо, без выдуманного журнала сервера.

Обязательность и допустимое содержимое

Колонка title имеет NOT NULL и проверку btrim(title) <> ''. Первое правило запрещает отсутствие значения. Второе запрещает пустую строку или строку из обычных пробелов после обрезки. Эти правила дополняют друг друга. Если оставить только проверку выражения, отсутствие не станет автоматически запрещённым: результат сравнения с NULL имеет особое значение.

В PostgreSQL условие CHECK считается удовлетворённым, когда выражение истинно или даёт неизвестное значение. Поэтому проверка положительного числа не заменяет NOT NULL. Для planned_lessons оба требования записаны рядом:

planned_lessons integer NOT NULL CHECK (planned_lessons > 0)

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

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

Уникальность и внешний ключ

Публичный код курса slug имеет UNIQUE NOT NULL. Уникальность запрещает два одинаковых кода в этой таблице. Обязательность не разрешает обойти договор через отсутствующий код. Отдельно действует проверка выбранного ASCII-формата. Каждый элемент выражает свою причину: найти конкретный объект, иметь значение для поиска и не получить случайные пробелы в коде.

Обычная уникальность в PostgreSQL сама по себе не запрещает несколько NULL. В нашей модели этого случая нет благодаря NOT NULL. Если позже появится необязательный внешний код поставщика, поведение отсутствующих значений нужно выбрать отдельно, а не предполагать, что слово UNIQUE уже означает единственную пустую строку. Подробности правил приведены в руководстве ограничений.

Внешний ключ topic_id гарантирует, что указанная тема существует. Число девять допустимо для типа bigint, но в исходных четырёх темах такого ключа нет. Если попытаться привязать к нему курс, сервер должен отклонить изменение. Предварительный поиск темы приложением полезен для интерфейса, однако между поиском и записью другой клиент мог изменить данные. Связь в базе проверяется в рамках самой операции.

Урок связан с курсом аналогично, но его позиция уникальна только внутри родителя:

UNIQUE (course_id, position)

Первые главы JavaScript и HTML могут обе иметь позицию один. Две первые главы JavaScript не могут. Такое ограничение помогает редактору при перестановке оглавления: конфликт означает несовместимый порядок конкретного курса, а не запрет на повторённую цифру во всей библиотеке. Перестановки и транзакции нужно проектировать с учётом этого договора.

Правило публикации

В таблице курса статус равен либо draft, либо published. Для опубликованного курса обязателен момент публикации, для черновика он отсутствует. Связь между колонками закрепляет именованное ограничение:

CONSTRAINT course_publication
CHECK ((status = 'published') = (published_at IS NOT NULL))

Левая часть отвечает, опубликован ли курс. Правая отвечает, задан ли момент. Равенство требует совпадения ответов. Наличие NOT NULL у статуса и отдельной проверки допустимых статусов важно: без них смысл левой части мог бы расшириться непредусмотренным образом. Полный набор правил читается совместно, а не как случайная строка из схемы.

В исходных данных web-performance — черновик с отсутствующим временем. js-browser — опубликованный курс с заданным моментом. Если изменить только статус черновика на опубликованный, оставив время пустым, запись будет отклонена. Если одновременно передать оба согласованных значения, соответствующее ограничение не мешает изменению. Это показывает полезное свойство модели: переход состояния описывается единым изменением строки.

Проверка не подтверждает наличие готового текста всех уроков. Мы намеренно не объявляли такой межтабличный инвариант. У каждого курса только две учебные строки, а план может содержать двадцать глав. Статус в примере относится к доступности карточки курса, не к утверждению «вся серия завершена». Если продукт требует иного значения публикации, сначала изменяют предметный договор.

Удаление и понятная ошибка

При удалении темы используется ON DELETE RESTRICT: тема с курсами не исчезает автоматически. При удалении курса используется каскад на уроки, поскольку учебная глава вне родителя не существует. Каскад — выбранная политика, а не рекомендация удалять реальные материалы сайта. Для живой библиотеки вместо удаления может потребоваться архивное состояние и сохранение маршрутов.

Чтобы прочитать установленные правила без изменения базы, снимок содержит запрос к метаданным:

SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'catalog.courses'::regclass
ORDER BY conname;

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

Значение по умолчанию не отменяет ограничения. Если статус не передан, получится draft; если передан явно NULL, значение по умолчанию не подставляется вместо него. Это различие полезно при обработке формы: отсутствие поля в операции создания и сознательная попытка очистить обязательное поле имеют разный результат.

Где заканчивается проверка базы

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

Также база не сообщает, какой текст подсказки нужен пользователю. Ошибка уникального slug может означать случайный повтор отправки формы либо намеренную попытку создать другой курс под занятым кодом. Клиент знает контекст операции и может предложить открыть существующий объект, выбрать новый код или повторить чтение. Ограничение лишь надёжно предотвращает несовместимое сохранение.

В журнале полезно различать нарушения известных правил и неожиданные сбои, но не возвращать посетителю внутренние детали соединения. Учебный файл показывает определения напрямую, поскольку читатель изучает схему. Публичная форма имеет другой потребительский договор: понятная ошибка должна объяснять действие редактора, а не требовать от него читать pg_constraint.

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

Оглавление курса.