Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 22 из 25 ~18 мин +50 XP

Валидация схемы и уникальные индексы

Валидация схемы mongo — это validator на коллекции: документ $jsonSchema, который сервер проверяет на каждой вставке и обновлении, независимо от того, кто пишет. Заведём контракт через CreateCollection, поменяем на живой коллекции через collMod и RunCommand, разберём уровни strict и moderate, действия error и warn, ошибку 121 с деталями — и добавим unique index mongodb против дублей, потому что валидатор форму документа проверяет, а не совпадения.

Содержание урока · 10 разделов

Сценарий: сервис с проверкой полей в Go работает безупречно, а потом в коллекции появляются документы без email. Это ночной импорт из CSV, коллега с mongosh, скрипт на другом языке — любой клиент, который не обязан знать про проверки вашего кода. Валидация на стороне приложения обязательна, но она живёт в одном клиенте и не видит остальных писателей; валидация схемы mongo переносит последний рубеж на сервер: контракт живёт в коллекции, и обойти его нечем — любой insert и update проходит одну и ту же проверку. Это не замена проверок в коде, а второй рубеж: код отвечает за пользовательские сообщения и быстрые ответы, сервер — за целостность данных при любом источнике записи. Второй рубеж уже стоит: unique index mongodb из семнадцатого урока охраняет совпадения. Здесь добавим форму: обязательные поля, типы BSON, диапазоны чисел и регулярные выражения на строки — через документ $jsonSchema, который заводится при создании коллекции и меняется командой collMod. Нарушителя сервер встречает ошибкой 121 с подробностями, а Go-код ловит её теми же средствами, что и duplicate key в двадцатом уроке.

$jsonSchema: контракт на языке сервера#

Валидатор — обычный BSON-документ с оператором $jsonSchema внутри, а внутри него — подмножество стандарта JSON Schema с BSON-привкусом. Ключ bsonType описывает тип: object для корня документа, string для строк, int и long для целых, double и decimal для дробей, date для дат, objectId для идентификаторов — это BSON-типы, а не строки JSON, и int здесь значит int32 на проводе. Список required называет обязательные поля, словарь properties описывает схему каждого поля: для возраста — bsonType int с минимумом и максимумом, для email — строка с pattern, регулярным выражением, которому строка обязана соответствовать, для статуса — enum с набором допустимых значений. Важное свойство: валидатор — это предикат запроса, наравне с $gt и $exists, поэтому внутри легален любой оператор запроса, а сам $jsonSchema работает не только в описании коллекции, но и в find и в этапе $match конвейера — этим мы воспользуемся для аудита чуть ниже. Вариантов завода два: при создании коллекции — CreateCollection с опциями, для существующей — collMod через RunCommand. Уровни строгости и действия при нарушении настраиваются отдельными опциями, и о них следующий раздел.

CreateCollection с валидатором

GO
db := client.Database("shop")

validator := bson.M{
        "$jsonSchema": bson.M{
                "bsonType": "object",
                "required": bson.A{"email", "created_at"},
                "properties": bson.M{
                        "email": bson.M{"bsonType": "string"},
                        "age": bson.M{
                                "bsonType": "int",
                                "minimum":  0,
                                "maximum":  130,
                        },
                },
        },
}

err := db.CreateCollection(ctx, "users",
        options.CreateCollection().
                SetValidator(validator).
                SetValidationLevel("strict").
                SetValidationAction("error"))
if err != nil {
        return fmt.Errorf("create users collection: %w", err)
}

options.CreateCollection() собирает опции цепочкой Set-методов: SetValidator кладёт контракт, SetValidationLevel и SetValidationAction задают строгость и реакцию — пока строками strict и error. CreateCollection создаёт новую коллекцию с валидатором за один вызов; для существующего имени команда упадёт ошибкой NamespaceExists, поэтому живые коллекции с валидатором меняются только через collMod.

Уровни и действия: strict, moderate, warn#

Параметр validationLevel отвечает за то, кого проверяют. strict — все вставки и все обновления без исключений, значение по умолчанию. moderate — вставки проверяются все, а обновления — только тех документов, которые уже удовлетворяют схеме: документ, родившийся до введения валидатора и не прошедший бы проверку, можно обновлять по полям, не входящим в конфликт с новым контрактом. Это спасает коллекции с легаси-данными: strict на живой коллекции с нарушителями способен заблокировать рядовые обновления, потому что сервер перепроверяет документ целиком, а не только изменённые поля. off выключает проверку совсем — опция для временного отключения без удаления валидатора. Параметр validationAction отвечает за реакцию: error отклоняет нарушителя ошибкой — это дефолт и правильный выбор для новых коллекций; warn пишет предупреждение в журнал сервера, но документ сохраняет. Режим warn полезен на одном этапе: перед включением строгого контракта погоняйте день-другой в warn и посчитайте предупреждения — это честная статистика того, сколько документов strict отрезал бы. Полагаться на warn постоянно не стоит: без мониторинга журнала он превращается в декорацию, и данные тихо плывут дальше. Оба параметра — обычные строки в SetValidationLevel и SetValidationAction, и при смене через collMod они передаются ключами validationLevel и validationAction.

collMod: валидатор на живой коллекции#

CreateCollection создаёт коллекцию с валидатором, но только новую: повторный вызов на существующем имени упадёт с ошибкой NamespaceExists, а «перезаписать» контракт он не умеет. Живую коллекцию меняют командой collMod через db.RunCommand: документ команды с ключами collMod, validator и, при желании, validationLevel и validationAction. Здесь стоит нюанс драйвера v2: RunCommand принимает только упорядоченный документ — bson.D, и карта bson.M для команды не годится, что прямо сказано в документации метода. Возвращается *SingleResult: метод Err отдаёт статус команды, Decode при желании разберёт ответ сервера. Текущий контракт читается обратно: ListCollectionSpecifications возвращает спецификации с полем Options типа bson.Raw, из которого Lookup по ключу validator достаёт действующий документ — удобно проверять тестом, что контракт на месте после миграции. Тот же collMod в mongosh — db.runCommand({collMod: ...}) — ведёт себя одинаково: контракты не завязаны на язык клиента, поэтому править их можно любым инструментом, а поведение сверять между mongosh и Go.

collMod через RunCommand и ловля кода 121

GO
res := db.RunCommand(ctx, bson.D{
        {Key: "collMod", Value: "users"},
        {Key: "validator", Value: validator},
        {Key: "validationLevel", Value: "strict"},
})
if err := res.Err(); err != nil {
        return fmt.Errorf("collMod users: %w", err)
}

_, err := db.Collection("users").InsertOne(ctx, bson.M{
        "name": "документ без email",
})
var se mongo.ServerError
if errors.As(err, &se) && se.HasErrorCode(121) {
        log.Print("validator rejected the document: 121")
}

RunCommand принимает bson.D с командой collMod: ключи validator и validationLevel передают новый контракт живой коллекции. SingleResult.Err проверяет статус команды. Вставка-нарушитель получает WriteError кода 121 — DocumentValidationFailure; ServerError достаётся через errors.As из любой обёртки, а HasErrorCode сравнивает код без вскрытия структур.

Код 121: DocumentValidationFailure и Details#

На нарушение валидатора сервер отвечает кодом 121 — у него есть имя DocumentValidationFailure, но в Go код сравнивается числом. У одиночных вставок и обновлений это WriteException со срезом WriteErrors, у InsertMany и BulkWrite — BulkWriteException; правила те же, что выучены в двадцатом уроке про duplicate key: пачка unordered продолжает вставку и собирает все нарушения, ordered останавливается на первой. Проверка кода — метод HasErrorCode(121) интерфейса mongo.ServerError, который достаётся errors.As через любую цепочку обёрток; альтернатива — руками вскрыть WriteErrors и сравнить поле Code, но HasErrorCode короче и покрывает оба типа исключений. Самое ценное лежит в поле Details у WriteError: сервер кладёт туда errInfo — документ с идентификатором нарушителя и деревом несработавших правил схемы, по которому видно и поле, и причину. Драйвер errInfo не парсит — читайте bson.Raw через Lookup, когда нужно отдать пользователю внятную причину, а не общий 400. Отдельно держите в голове warn-режим: там ошибки не будет вовсе, только строки в журнале сервера, и нарушения ищутся не в коде, а в логах. В общей карте ошибок 121 — сосед кода 11000: дубликат говорит «такой ключ уже занят», валидация — «документ не той формы», и оба превращаются в доменные ошибки на границе репозитория.

Параметры валидации одним взглядом#

ПараметрЧто делаетКогда выбирать
SetValidationLevel("strict")проверяет все вставки и обновлениядефолт для новых коллекций
SetValidationLevel("moderate")не блокирует обновления уже-невалидных документовколлекции с легаси-данными
SetValidationAction("error")отклоняет нарушителя WriteError 121дефолт; контракт обязателен
SetValidationAction("warn")предупреждение в журнал, документ сохраняетсяпереходный период перед strict
RunCommand с collModменяет валидатор живой коллекцииCreateCollection — только для новой

Аудит: найти нарушителей до их блокировки#

Поскольку $jsonSchema — обычный оператор запроса, им можно искать документы, которые схеме соответствуют. Инверсия — $nor с $jsonSchema внутри — выбирает всё, что контракту не удовлетворяет: db.users.find с таким предикатом вернёт ровно нарушителей. Это дешёвый аудит перед ужесточением: сначала collMod в warn-режиме, затем посчитать и разглядеть нарушителей запросом, затем включить strict, когда счётчик уйдёт в ноль. Тот же приём находит и обратное — документы, которые контракту соответствуют, если предикат применить без инверсии; для быстрой оценки объёма достаточно countDocuments вместо полной выборки. В конвейере агрегации предикат живёт в этапе $match, поэтому отчёт по нарушителям с группировкой по полям собирается тем же инструментом, что и любой другой отчёт. Аудит-запросы выполняйте на вторичном узле с read preference secondary, если коллекция большая: скан по всей коллекции для отчёта не должен мешать записи. И помните про explain из урока про индексы: $jsonSchema в фильтре не опирается на индексы и честно сканирует — для разового аудита это нормально, для панельного запроса — нет.

Правила валидации схемы одним списком#

  • Валидатор — предикат сервера: $jsonSchema с bsonType, required, properties, minimum, pattern, enum.
  • Новая коллекция — CreateCollection с SetValidator; живая — collMod через db.RunCommand.
  • RunCommand в v2 принимает только упорядоченный bson.D: карта bson.M для команды не годится.
  • strict и error — дефолт; moderate выбирают для коллекций с легаси-документами, warn — на переходный период.
  • collMod существующие документы не перевалидирует: нарушители проявятся 121 на первом обновлении.
  • Нарушение — WriteError 121 через HasErrorCode; уникальность валидатором не решается — нужен unique index mongodb.

Проверка: вставка нарушителя

Коллекция создана с validationLevel strict и validationAction error. Что вернёт InsertOne документа без обязательного поля email?

Проверка: смена контракта

Коллекция orders существует и наполнена данными. Как надеть на неё валидатор?

Что напечатает пара вставок?

Прочитайте код и выберите, что он выведет:

// коллекция: required ["email"], strict, error
_, err1 := coll.InsertOne(ctx, bson.M{"email": "anna@example.com"})
_, err2 := coll.InsertOne(ctx, bson.M{"name": "без почты"})

var se mongo.ServerError
_ = errors.As(err2, &se)
fmt.Println(err1 == nil, se.HasErrorCode(121))

Соберите создание коллекции с валидатором

Восстановите вызов: создание коллекции, сборщик опций, валидатор и строгий уровень.

err := db.(ctx, "users", options.(). Set(validator). SetValidationLevel(""). SetValidationAction("error")) if err != nil { return fmt.Errorf("create users: %w", err) }

Соберите смену валидатора

Расставьте строки метода: документ команды collMod, вызов RunCommand, проверка статуса.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: валидация схемы

Термин

$jsonSchema

нажмите, чтобы перевернуть
Определение

Валидатор-документ: bsonType, required, properties, minimum, pattern, enum. Обычный оператор запроса: работает в описании коллекции, в find и в этапе $match конвейера.

1 / 6

Контракт стоит в базе: обязательные поля, типы и диапазоны проверяет сервер, нарушители получают 121, а уникальность держит отдельный рубеж — индекс. База данных теперь защищает сама, и сервис может сосредоточиться на сообщениях для людей. Дальше по курсу: урок про индексы стоит перечитать перед заводом unique index mongodb — там правило про упорядоченные Keys и цену каждого индекса для записи; урок про ошибки драйвера разложит 121 и 11000 по одной карте — HasErrorCode и IsDuplicateKeyError рядом; а в репозитории из двадцать первого урока обе ошибки превращаются в доменные на границе. Тем, кто работает с Postgres, полезно провести параллель: уникальность 23505 в pgx — та же идея «ошибка записи как бизнес-сигнал», только код другой. Проверьте всё руками: песочница сайта не исполняет mongo-go-driver, поэтому поднимите MongoDB локально в Docker, создайте коллекцию с валидатором из урока и вставьте документ без обязательного поля — код 121 и детали в Details вы увидите своими глазами.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Валидатор — документ $jsonSchema на коллекции: bsonType, required, properties, minimum, pattern, enum.
  • Новая коллекция — CreateCollection с SetValidator; живая — collMod через db.RunCommand с bson.D.
  • validationLevel: strict — все записи, moderate — щадит легаси-нарушителей, off — выключено; дефолт strict.
  • validationAction: error отклоняет кодом 121, warn пишет в журнал и сохраняет; дефолт error.
  • collMod не перевалидирует старые документы: нарушители проявятся 121 на первом обновлении.
  • Код 121 ловится HasErrorCode через mongo.ServerError; серверные подробности — в Details у WriteError.
  • Уникальность — свойство коллекции: только unique index mongodb с SetUnique(true) и IsDuplicateKeyError.
  • $jsonSchema — предикат запроса: $nor с ним находит нарушителей перед включением strict.

Домашнее задание

Поднимите MongoDB и создайте коллекцию users с валидатором из урока: обязательные email и created_at, возраст от нуля до ста тридцати. Вставьте валидный документ, затем три нарушителя: без email, с отрицательным возрастом и с email-числом — каждый раз прочитайте Details у WriteError и найдите failingDocumentId. Переведите коллекцию в warn, вставьте нарушителя и найдите предупреждение в журнале сервера. Затем через collMod поставьте moderate, обновите валидный документ и убедитесь, что обновление прошло, — после этого примените strict к коллекции с нарушителем и повторите.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Валидация схемы и уникальные индексы)

Вопросы и ответы по уроку

Нужна ли валидация схемы, если все проверки уже в Go?

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

Почему bson.M не подходит для RunCommand?

Команда — упорядоченный документ: имя команды обязано идти первым ключом, и сервер читает его первым. У карты порядок ключей не определён, поэтому RunCommand в v2 принимает только упорядоченные типы вроде bson.D — это прямо написано в документации метода. bson.M остаётся для фильтров и валидаторов, где порядок не важен.

Что выбрать для коллекции с унаследованными данными?

Начните с warn при strict-уровне: неделя-другая даст честную статистику нарушителей. Затем проведите аудит $nor-запросом и перепишите или почистите нарушителей. После этого строгий уровень и действие error заблокируют только реальные ошибки. Если чистить легаси нельзя, оставьте moderate: обновления старых документов не встанут.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

zerolog25 уроков~12 чпрактика25 уроков уже доступны

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос