Статьи

Проверка схемы JSON и синтаксис выразительных запросов в MongoDB 3.6

Эта статья была первоначально опубликована на MongoDB . Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.

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

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

MongoDB 3.2 был первым выпуском, в котором была представлена проверка документов , одна из функций, которую требовали разработчики и администраторы баз данных, привыкшие к реляционным базам данных. Как отметил главный технический директор MongoDB, Элиот Горовиц в разделе «Проверка документов и что означают динамические схемы» :

Наряду с остальными функциями 3.2 «схемы, когда вам это нужно», проверка документов дает MongoDB новый, мощный способ поддерживать чистоту данных. Это определенно не последний набор инструментов, который мы предоставим, но довольно важный шаг в том, как MongoDB обрабатывает схему .

Объявление о поддержке проверки схемы JSON

Основываясь на функциональности проверки документов MongoDB 3.2, MongoDB 3.6 представляет более мощный способ принудительного применения схем в базе данных с поддержкой JSON Schema Validation, спецификации, которая является частью нового стандарта JSON Schema IETF.

Валидация схемы JSON расширяет валидацию документов различными способами, включая возможность принудительного применения схем внутри массивов и предотвращения добавления неутвержденных атрибутов. Это новые функции, которые мы рассмотрим в этом посте, а также возможность создавать правила проверки бизнеса.

Начиная с MongoDB 3.6, схема JSON является рекомендуемым способом обеспечения проверки схемы . В следующем разделе описываются функции и преимущества использования проверки схемы JSON.

Переход от проверки документа к проверке схемы JSON

Мы начнем с создания коллекции заказов (на основе примера, который мы опубликовали в публикации блога учебного пособия по проверке документов ):

db.createCollection("orders", { validator: { item: { $type: "string" }, price: { $type: "decimal" } } }); 

С помощью этой конфигурации проверки документа мы не только гарантируем, что атрибуты товара и цены присутствуют в любом документе заказа , но и что товар является строкой, а цена — десятичной (это рекомендуемый тип для всех валютных и процентных значений). Поэтому следующий элемент не может быть вставлен (из-за ценового атрибута «мошенник»):

 db.orders.insert({ "_id": 6666, "item": "jkl", "price": "rogue", "quantity": 1 }); 

Однако следующий документ может быть вставлен (обратите внимание на атрибут «pryce» с ошибкой):

 db.orders.insert({ "_id": 6667, "item": "jkl", "price": NumberDecimal("15.5"), "pryce": "rogue" }); 

До MongoDB 3.6 вы не могли предотвратить добавление ошибочных или несанкционированных атрибутов. Давайте посмотрим, как проверка схемы JSON может предотвратить такое поведение. Для этого мы будем использовать новый оператор $ jsonSchema :

 db.runCommand({ collMod: "orders", validator: { $jsonSchema: { bsonType: "object", required: ["item", "price"], properties: { item: { bsonType: "string" }, price: { bsonType: "decimal" } } } } }); 

Приведенная выше схема JSON является точным эквивалентом правила проверки документа, которое мы ранее установили в коллекции заказов . Давайте проверим, что наша схема действительно была обновлена ​​для использования нового оператора $ jsonSchema с помощью метода db.getCollectionInfos () в оболочке Mongo:

 db.getCollectionInfos({name:"orders"}) 

Эта команда выводит множество информации о сборе заказов . Для удобства чтения ниже приведен раздел, содержащий схему JSON:

 ... "options" : { "validator" : { "$jsonSchema" : { "bsonType" : "object", "required" : [ "item", "price" ], "properties" : { "item" : { "bsonType" : "string" }, "price" : { "bsonType" : "decimal" } } } }, "validationLevel" : "strict", "validationAction" : "error" } ... 

Теперь давайте немного обогатим нашу схему JSON, чтобы лучше использовать ее мощные функции:

 db.runCommand({ collMod: "orders", validator: { $jsonSchema: { bsonType: "object", <strong>additionalProperties: false</strong>, required: ["item", "price"], properties: { <strong>_id: {}</strong>, item: { bsonType: "string", description: "'item' must be a string and is required" }, price: { bsonType: "decimal", description: "'price' must be a decimal and is required" }, quantity: { <strong>bsonType: ["int", "long"]</strong>, minimum: 1, maximum: 100, exclusiveMaximum: true, description: "'quantity' must be short or long integer between 1 and 99" } } } } }); 

Давайте рассмотрим дополнения, которые мы внесли в нашу схему:

  • Во-первых, обратите внимание на использование атрибута AdditionalProperties: false : он не позволяет нам добавлять какие-либо атрибуты, кроме тех, которые упомянуты в разделе свойств . Например, больше не будет возможности вставлять данные, содержащие атрибут pryce с ошибкой. В результате использование AdditionalProperties: false на корневом уровне документа также делает обязательным объявление свойства _id : независимо от того, явно ли наш код вставки устанавливает его или нет, это поле требуется MongoDB и будет создано автоматически, если нет настоящее время. Таким образом, мы должны явно включить его в раздел свойств нашей схемы.
  • Во-вторых, мы решили объявить атрибут количества как короткое или длинное целое число от 1 до 99 (используя атрибуты минимальный , максимальный и эксклюзивный максимальный ). Конечно, поскольку наша схема допускает только целые числа меньше 100, мы могли бы просто установить для свойства bsonType значение int . Но добавление допустимого типа делает код приложения более гибким, особенно если могут быть планы по снятию максимального ограничения.
  • Наконец, обратите внимание, что атрибут description (присутствующий в объявлениях атрибутов item , price и количеств ) совершенно необязателен и не влияет на схему, кроме документирования схемы для читателя.

По приведенной выше схеме в нашу коллекцию заказов можно вставить следующие документы:

 db.orders.insert({ "item": "jkl", "price": NumberDecimal(15.50), "quantity": NumberInt(99) }); db.orders.insert({ "item": "jklm", "price": NumberDecimal(15.50), "quantity": NumberLong(99) }); 

Однако следующие документы больше не считаются действительными:

 db.orders.insert({ "item": "jkl", "price": NumberDecimal(15.50), <strong>"quantity": NumberInt(100)</strong> }); db.orders.insert({ "item": "jkl", "price": NumberDecimal(15.50), <strong>"quantity": "98"</strong> }); db.orders.insert({ "item": "jkl", <strong>"pryce": NumberDecimal(15.50),</strong> "quantity": NumberInt(99) }); 

Вы, наверное, заметили, что наши заказы выше кажутся странными: они содержат только один элемент. Более реалистично, заказ состоит из нескольких элементов, и возможная структура JSON может быть следующей:

 { _id: 10000, total: NumberDecimal(141), VAT: 0.20, totalWithVAT: NumberDecimal(169), lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), unit_price:NumberDecimal(9) }, { sku: "MDBTS002", quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] } 

С MongoDB 3.6 теперь мы можем управлять структурой массива lineitems , например, с помощью следующей схемы JSON:

 db.runCommand({ collMod: "orders", validator: { $jsonSchema: { bsonType: "object", required: ["lineitems"], properties: { lineitems: { <strong>bsonType: ["array"],</strong> minItems: 1, maxItems:10, items: { required: ["unit_price", "sku", "quantity"], bsonType: "object", additionalProperties: false, properties: { sku: { bsonType: "string", description: "'sku' must be a string and is required" }, name: { bsonType: "string", description: "'name' must be a string" }, unit_price: { bsonType: "decimal", description: "'unit_price' must be a decimal and is required" }, quantity: { bsonType: ["int", "long"], minimum: 0, maximum: 100, exclusiveMaximum: true, description: "'quantity' must be a short or long integer in [0, 100)" }, } } } } } } }); 

С помощью приведенной выше схемы мы обеспечиваем, чтобы любой заказ, вставленный или обновленный в коллекции заказов, содержал массив lineitems от 1 до 10 документов, каждый из которых unit_price атрибуты sku , unit_price и unit_price (количество должно быть целым числом).

Схема не позволит вставить следующий плохо сформированный документ:

 db.orders.insert({ total: NumberDecimal(141), VAT: NumberDecimal(0.20), totalWithVAT: NumberDecimal(169), lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), price: NumberDecimal(9) //this should be 'unit_price' }, { name: "MDBTS002", //missing a 'sku' property quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] }) 

Но это позволило бы вставить следующий, совместимый со схемой документ:

 db.orders.insert({ total: NumberDecimal(141), VAT: NumberDecimal(0.20), totalWithVAT: NumberDecimal(169), lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), unit_price: NumberDecimal(9) }, { sku: "MDBTS002", quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] }) 

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

  1. totalWithVAT значение атрибута totalWithVAT (оно должно быть равно 141 * 1.20 = 169.2)
  2. Неверное значение total атрибута (оно должно быть равно сумме каждого промежуточного итога позиции (т. Е. 10 * 9 + 10 * 5 = 140).

Есть ли способ обеспечить totalWithVAT значений total и totalWithVAT , используя правила проверки базы данных, не полагаясь исключительно на логику приложения?

Знакомство с синтаксисом выразительных запросов MongoDB

Добавление более сложных правил проверки бизнеса теперь возможно благодаря выразительному синтаксису запросов, новой функции MongoDB 3.6.

Одна из целей синтаксиса выразительных запросов — донести всю мощь выражений агрегации MongoDB до языка запросов MongoDB. Интересным примером использования является возможность составления правил динамической проверки, которые вычисляют и сравнивают несколько значений атрибутов во время выполнения. Используя новый оператор $ expr , можно проверить значение атрибута totalWithVAT с помощью следующего выражения проверки:

 $expr: { $eq: [ "$totalWithVAT", {$multiply: [ "$total", {$sum: [1, "$VAT"]} ]} ] } 

Приведенное выше выражение проверяет, что totalWithVAT атрибута totalWithVAT равно total * (1+VAT) . В его компактной форме, вот как мы могли бы использовать его в качестве правила проверки наряду с нашей проверкой схемы JSON:

 db.runCommand({ collMod: "orders", validator: { <strong>$expr:{$eq:[ "$totalWithVAT", {$multiply:["$total", {$sum:[1,"$VAT"]}]} ]}</strong>, $jsonSchema: { bsonType: "object", required: ["lineitems"], properties: { lineitems: { bsonType: ["array"], minItems: 1, maxItems:10, items: { required: ["unit_price", "sku", "quantity"], bsonType: "object", additionalProperties: false, properties: { sku: { bsonType: "string", description: "'sku' must be a string and is required" }, name: { bsonType: "string", description: "'name' must be a string" }, unit_price: { bsonType: "decimal", description: "'unit_price' must be a decimal and is required" }, quantity: { bsonType: ["int", "long"], minimum: 0, maximum: 100, exclusiveMaximum: true, description: "'quantity' must be a short or long integer in [0, 100)" }, } } } } } } }); 

При использовании вышеуказанного валидатора следующая операция вставки больше невозможна:

 db.orders.insert({ total: NumberDecimal(141), VAT: NumberDecimal(0.20), totalWithVAT: NumberDecimal(169), lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), Unit_price: NumberDecimal(9) }, { sku: "MDBTS002", quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] }) 

Вместо totalWithVAT значение totalWithVAT должно быть скорректировано в соответствии с нашим новым правилом проверки НДС:

 db.orders.insert({ total: NumberDecimal(141), VAT: NumberDecimal(0.20), <strong>totalWithVAT: NumberDecimal(169.2)</strong>, lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), unit_price: NumberDecimal(9) }, { sku: "MDBTS002", quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] }) 

Если мы также хотим убедиться, что итоговое значение является суммой каждого значения элемента строки заказа (т.е. количества unit_price *), следует использовать следующее выражение:

 $expr: { $eq: [ "$total", {$sum: { $map: { "input": "$lineitems", "as": "item", "in": { "$multiply": [ "$$item.quantity", "$$item.unit_price" ] } } }} ] } 

Вышеприведенное выражение использует оператор $ map для вычисления промежуточного итога каждого элемента строки, затем суммирует все эти промежуточные итоги и, наконец, сравнивает его с итоговым значением. Чтобы убедиться, что проверены правила проверки итогов и НДС, мы должны объединить их, используя $ и оператор. Наконец, наш валидатор коллекции может быть обновлен следующей командой:

 db.runCommand({ collMod: "orders", validator: { $expr:{ $and:[ {$eq:[ "$totalWithVAT", {$multiply:["$total", {$sum:[1,"$VAT"]}]} ]}, {$eq: [ "$total", {$sum: {$map: { "input": "$lineitems", "as": "item", "in":{"$multiply":["$$item.quantity","$$item.unit_price"]} }}} ]} ]}, $jsonSchema: { bsonType: "object", required: ["lineitems", "total", "VAT", "totalWithVAT"], properties: { total: { bsonType: "decimal" }, VAT: { bsonType: "decimal" }, totalWithVAT: { bsonType: "decimal" }, lineitems: { bsonType: ["array"], minItems: 1, maxItems:10, items: { required: ["unit_price", "sku", "quantity"], bsonType: "object", additionalProperties: false, properties: { sku: {bsonType: "string"}, name: {bsonType: "string"}, unit_price: {bsonType: "decimal"}, quantity: { bsonType: ["int", "long"], minimum: 0, maximum: 100, exclusiveMaximum: true }, } } } } } } }); 

Соответственно, мы должны обновить свойства total и totalWithVAT чтобы соответствовать нашей обновленной схеме и правилам проверки бизнеса (без изменения массива lineitems ):

 db.orders.insert({ total: NumberDecimal(140), VAT: NumberDecimal(0.20), totalWithVAT: NumberDecimal(168), lineitems: [ { sku: "MDBTS001", name: "MongoDB Stitch T-shirt", quantity: NumberInt(10), unit_price: NumberDecimal(9) }, { sku: "MDBTS002", quantity: NumberInt(5), unit_price: NumberDecimal(10) } ] }) 

Следующие шаги

С введением JSON Schema Validation в MongoDB 3.6 администраторы баз данных теперь лучше подготовлены для удовлетворения требований к управлению данными, исходящих от специалистов по соответствию или регуляторов, и при этом получают выгоду от гибкой архитектуры схемы MongoDB.

Кроме того, разработчики найдут новый синтаксис выразительных запросов полезным для упрощения своей базы кода приложения, перенеся бизнес-логику с уровня приложения на уровень базы данных.

Если вы хотите узнать больше обо всем новом в MongoDB 3.6, загрузите наше руководство «Что нового?» .

Если вы хотите углубиться в техническую сторону, посетите страницы проверки схемы и синтаксиса выразительных запросов в нашей официальной документации.

Если вы хотите получить более практический, практический опыт, взгляните на эту практическую лабораторию JSON Schema Validation . Вы можете попробовать его прямо сейчас в службе базы данных MongoDB Atlas , которая поддерживает MongoDB 3.6 с момента его общей доступности.

И последнее, но не менее важное: подпишитесь на бесплатное обучение MongoDB 3.6 в университете MongoDB.