В прошлом уроке мы копировали поля и хранили вычисленные значения, и всё держалось на аккуратности приложения. Но приложение ошибается: один разработчик запишет цену числом, другой строкой, третий забудет название. Сегодня научим MongoDB проверять документы при записи, а потом пройдёмся по ошибкам моделирования, на которые натыкаются чаще всего. Это последний урок кластера про моделирование.
Гибкая схема не значит отсутствие схемы
Представь склад, куда принимают коробки любой формы. Удобно: можно привезти что угодно. Но через год никто не знает, где что лежит, а половина коробок подписана неразборчиво. Опытный кладовщик всё равно заводит правила приёмки: на коробке должна быть наклейка с названием и весом.
С MongoDB так же. Ей не нужна заранее объявленная структура, как CREATE TABLE в PostgreSQL. Но у приложения всё равно есть ожидания: у книги должно быть название, цена должна быть числом. Если база их не проверяет, ошибки копятся молча.
Вот как это выглядит. В коллекции books никаких правил нет, и кто-то добавил книгу с ценой строкой:
db.books.insertOne({ _id: 10, title: 'Generation П', genre: 'роман', price: '700', pages: 320, year: 1999 })
{ acknowledged: true, insertedId: 10 }
Теперь витрина ищет книги дороже 500 рублей:
db.books.find({ price: { $gt: 500 } }, { title: 1, price: 1 }).sort({ _id: 1 })
[
{ _id: 1, title: 'Мастер и Маргарита', price: 690 },
{ _id: 4, title: 'Трудно быть богом', price: 520 },
{ _id: 6, title: 'Мёртвые души', price: 610 },
{ _id: 7, title: 'Грокаем алгоритмы', price: 1290 },
{ _id: 8, title: 'Совершенный код', price: 2490 }
]
Книги за 700 рублей нет. MongoDB сравнивает значения одного типа, и строка '700' не больше числа 500 и не меньше его: она не участвует в сравнении. Мы встречали эту ловушку в уроке про операторы запроса. Найти таких нарушителей помогает оператор $type:
db.books.find({ price: { $type: 'string' } }, { title: 1, price: 1 })
[ { _id: 10, title: 'Generation П', price: '700' } ]
Искать нарушения после записи поздно. Лучше не пускать их в базу.
Правила приёмки: $jsonSchema
Правила проверки задают при создании коллекции в параметре validator. Чаще всего их пишут на языке JSON Schema через оператор $jsonSchema. Заведём коллекцию издательств с правилами:
db.createCollection('publishers', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['name', 'country'],
properties: {
name: { bsonType: 'string', minLength: 2 },
country: { enum: ['Россия', 'США', 'Великобритания'] },
founded: { bsonType: 'int', minimum: 1700 }
}
}
}
})
{ ok: 1 }
Разберём правила:
required: поля, без которых документ не примут;properties: правила для отдельных полей;bsonType: тип поля в терминах BSON, например'string','int','double','date','array';minLength,minimum: нижние границы для строк и чисел;enum: список допустимых значений.
Поля, которые не упомянуты в схеме, по-прежнему разрешены. Схема описывает обязательный минимум, а не полный список полей. Запретить лишние поля можно правилом additionalProperties: false.
Правильный документ проходит как обычно:
db.publishers.insertOne({ _id: 1, name: 'АСТ', country: 'Россия', founded: 1990 })
{ acknowledged: true, insertedId: 1 }
А вот документ без страны сервер отвергает:
db.publishers.insertOne({ _id: 2, name: 'Питер', founded: 1991 })
Uncaught:
MongoServerError: Document failed validation
Additional information: {
failingDocumentId: 2,
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{
operatorName: 'required',
specifiedAs: { required: [ 'name', 'country' ] },
missingProperties: [ 'country' ]
}
]
}
}
Ответ читается сверху вниз: валидация не пройдена, документ с номером 2, нарушено правило required, не хватает поля country. Если нарушений несколько, сервер перечислит их все:
db.publishers.insertOne({ _id: 3, name: 'Х', country: 'Франция' })
Uncaught:
MongoServerError: Document failed validation
Additional information: {
failingDocumentId: 3,
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{
operatorName: 'properties',
propertiesNotSatisfied: [
{
propertyName: 'name',
details: [
{
operatorName: 'minLength',
specifiedAs: { minLength: 2 },
reason: 'specified string length was not satisfied',
consideredValue: 'Х'
}
]
},
{
propertyName: 'country',
details: [
{
operatorName: 'enum',
specifiedAs: { enum: [ 'Россия', 'США', 'Великобритания' ] },
reason: 'value was not found in enum',
consideredValue: 'Франция'
}
]
}
]
}
]
}
}
Проверка работает и при обновлении. Попробуем записать год основания словами:
db.publishers.updateOne({ _id: 1 }, { $set: { founded: 'девяностые' } })
Uncaught:
MongoServerError: Plan executor error during update :: caused by :: Document failed validation
Additional information: {
failingDocumentId: 1,
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{
operatorName: 'properties',
propertiesNotSatisfied: [
{
propertyName: 'founded',
details: [
{
operatorName: 'bsonType',
specifiedAs: { bsonType: 'int' },
reason: 'type did not match',
consideredValue: 'девяностые',
consideredType: 'string'
}
]
}
]
}
]
}
}
Одна тонкость с числами. mongosh записывает целое число вроде 1990 как int, а дробное вроде 1990.5 как double. Если поле может быть любым числом, пиши bsonType: ['int', 'double'].
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.