Содержание урока · 10 разделов
- $jsonSchema: контракт на языке сервера
- Уровни и действия: strict, moderate, warn
- collMod: валидатор на живой коллекции
- Код 121: DocumentValidationFailure и Details
- Параметры валидации одним взглядом
- Аудит: найти нарушителей до их блокировки
- Правила валидации схемы одним списком
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сценарий: сервис с проверкой полей в 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 с валидатором
GOdb := 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 — команда прошла без ошибок, и все решили, что коллекция теперь чистая. Через месяц обновление старого документа без email падает кодом 121 прямо в пользовательском флоу: strict перепроверяет документ целиком при каждом обновлении, а легаси-нарушители спокойно лежали в базе с момента применения контракта — collMod существующие документы не сканирует и не перевалидирует. Если в коллекции есть документы старше валидатора, выбирайте moderate или планируйте переписывание нарушителей; и гоняйте смену контракта сначала на staging-копии.
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
GOres := 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 — только для новой |
Команда закрыла требование «email уникален» валидатором схемы: поле обязательное, тип строка — защита вроде бы есть. Ночной импорт создал два аккаунта на один адрес, и оба прошли проверку формы: $jsonSchema видит только один документ и не сравнивает его с соседями. Если в задании слово «уникальный», то это свойство коллекции, а не документа, и несёт его только unique index mongodb через SetUnique(true), а ловится отказ mongo.IsDuplicateKeyError. Валидатор и индекс — разные рубежи: форма и совпадения, и нужны оба сразу.
Аудит: найти нарушителей до их блокировки#
Поскольку $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))Соберите создание коллекции с валидатором
Восстановите вызов: создание коллекции, сборщик опций, валидатор и строгий уровень.
Соберите смену валидатора
Расставьте строки метода: документ команды collMod, вызов RunCommand, проверка статуса.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: валидация схемы
Контракт стоит в базе: обязательные поля, типы и диапазоны проверяет сервер, нарушители получают 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 к коллекции с нарушителем и повторите.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужна ли валидация схемы, если все проверки уже в Go?
Проверки в коде обслуживают только вашего клиента. Ночной импорт, скрипт на другом языке, коллега в mongosh пишут мимо — и сервер примет что угодно. Контракт в коллекции проверяет всех писателей одной проверкой. Код остаётся: он даёт быстрые и понятные сообщения пользователю до обращения к базе.
Почему bson.M не подходит для RunCommand?
Команда — упорядоченный документ: имя команды обязано идти первым ключом, и сервер читает его первым. У карты порядок ключей не определён, поэтому RunCommand в v2 принимает только упорядоченные типы вроде bson.D — это прямо написано в документации метода. bson.M остаётся для фильтров и валидаторов, где порядок не важен.
Что выбрать для коллекции с унаследованными данными?
Начните с warn при strict-уровне: неделя-другая даст честную статистику нарушителей. Затем проведите аудит $nor-запросом и перепишите или почистите нарушителей. После этого строгий уровень и действие error заблокируют только реальные ошибки. Если чистить легаси нельзя, оставьте moderate: обновления старых документов не встанут.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Zerolog в Go: быстрое JSON-логирование
Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.
- Писать структурированные JSON-логи с нуля аллокаций
- Выбирать уровни и поля события осознанно
- Настраивать ConsoleWriter для dev и JSON для прода
- Строить сублоггеры и контекстные поля на запрос