MongoDB 3.6中的JSON模式验证和富有表现力的查询语法

本文最初在MongoDB上发布。 感谢您支持使SitePoint成为可能的合作伙伴。

MongoDB的主要优势之一一直是增强开发人员的能力:依靠灵活的架构体系结构,MongoDB使应用程序可以更轻松快捷地从概念验证到生产的整个开发阶段,并根据需求的发展在更新周期中进行迭代。

但是,随着应用程序的成熟和扩展,它们趋于达到一个稳定的阶段,在此阶段,频繁的模式更改不再至关重要,或者必须以更受控制的方式推出,以防止将不良数据插入数据库中。 当多个应用程序写入同一数据库时,或者当分析过程依赖于预定义的数据结构来准确和有用时,这些控件尤其重要。

MongoDB 3.2是第一个引入文档验证的版本 ,它是习惯于关系数据库的开发人员和DBA不断要求的功能之一。 作为MongoDB的CTO,Eliot Horowitz在文档验证和动态模式的含义中强调了:

文档验证与3.2的其余“需要时的架构”功能一起,为MongoDB提供了一种新的,强大的方式来保持数据干净。 这些绝对不是我们将提供的最终工具集,而是MongoDB如何处理schema的重要一步

宣布JSON架构验证支持

在MongoDB 3.2的文档验证功能的基础上, MongoDB 3.6通过支持JSON Schema Validation(该规范是IETF新兴的JSON Schema标准的一部分)的支持,引入了一种更强大的数据库模式实施方法。

JSON模式验证以多种不同方式扩展了文档验证,包括在数组内强制执行模式并防止添加未经批准的属性的能力。 这些是我们将在此博客文章中重点介绍的新功能,以及建立业务验证规则的功能。

从MongoDB 3.6开始,建议使用JSON Schema来执行Schema Validation 。 下一节将重点介绍使用JSON模式验证的功能和好处。

从文档验证切换到JSON模式验证

我们将首先创建一个订单集合(基于我们在Document Validation教程博客文章中发布的示例):

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

使用此文档验证配置,我们不仅可以确保在任何订单文档中都同时存在商品价格属性,而且还可以确保商品是一个字符串, 价格是一个十进制(这是所有货币和百分比值的推荐类型)。 因此,不能插入以下元素(由于“ rogue” 价格属性):

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模式与我们之前在订单集合上设置的文档验证规则完全相同。 我们通过在Mongo shell中使用db.getCollectionInfos()方法,检查我们的架构是否确实已更新为使用新的$ jsonSchema运算符:

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属性的数据。 结果,在文档的根级别使用extraProperties:false也使_id属性的声明成为强制性的:无论我们的插入代码是否显式设置它,它都是MongoDB要求的字段,如果没有,它将自动创建当下。 因此,我们必须在模式的属性部分中明确地包含它。
  • 其次,我们选择将数量属性声明为1到99之间的短整数或长整数(使用minimummaximumExclusiveMaximum属性)。 当然,由于我们的架构仅允许小于100的整数,因此我们只需将bsonType属性设置为int即可 。 但是, 只要添加有效类型,就可以使应用程序代码更加灵活,尤其是在可能计划取消最大限制的情况下。
  • 最后,请注意, description属性(出现在itempricequantity属性声明中)是完全可选的,除了为读者记录架构之外,对架构没有任何影响。

使用上面的架构,可以将以下文档插入我们的订单集中:

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)"
                      },
                  }                    
              }
          }
        }
      }
    }
  });

使用上面的模式,我们强制要求在订单集合中插入或更新的任何订单都包含一个包含1至10个文档的lineitems数组,这些文档都具有skuunit_pricequantity属性(数量必须为整数)。

该架构将防止插入以下格式不正确的文档:

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属性值不正确(应等于141 * 1.20 = 169.2)
  2. total属性值不正确(它应该等于每个行项子总的总和,(即10×9 + 10 * 5 = 140)

有什么方法可以使用数据库验证规则来强制totaltotalWithVAT值正确,而不必完全依赖于应用程序逻辑吗?

引入MongoDB表达查询语法

由于MongoDB 3.6的一项新功能即表达性查询语法,现在可以添加更复杂的业务验证规则。

表达查询语法的目标之一是将MongoDB 聚合表达式的功能引入MongoDB的查询语言 。 一个有趣的用例是能够组成动态验证规则,该规则可以在运行时计算和比较多个属性值。 使用新的$ expr运算符,可以使用以下验证表达式来验证totalWithVAT属性的值:

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

上面的表达式检查totalWithVAT属性值是否等于total * (1+VAT) 。 在紧凑的形式中,这是我们如何将其用作验证规则以及JSON Schema验证:

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)
            }
        ]
})

相反,必须根据我们的新VAT验证规则调整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运算符计算每个订单项的小计,然后所有这些小计求和 ,最后其与总值 进行比较 。 为了确保同时检查了Total和VAT验证规则,我们必须使用$ and运算符将它们组合在一起。 最后,可以使用以下命令来更新我们的集合验证器:

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

                      },
                  }                    
              }
          }
        }
      }
    }
  });

因此,我们必须更新totaltotalWithVAT属性以符合我们更新的架构和业务验证规则(不更改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)
          }
      ]
  })

下一步

通过在MongoDB 3.6中引入JSON模式验证,数据库管理员现在可以更好地满足合规官或监管机构提出的数据治理要求,同时仍可从MongoDB的灵活模式架构中受益。

此外,开发人员会发现新的表达查询语法对于通过将业务逻辑从应用程序层移动到数据库层来使他们的应用程序代码库更简单有用。

如果您想了解有关MongoDB 3.6中所有新增功能的更多信息,请下载我们的新增功能指南

如果您想深入了解技术方面,请访问我们官方文档中的Schema ValidationExpressive Query Syntax页面。

如果您想获得更多实用的动手经验,请查看此JSON Schema Validation动手实验 。 您可以立即在MongoDB Atlas数据库服务上试用它,该服务自MongoDB 3.6正式发布之日起就支持它。

最后但并非最不重要的一点是,从MongoDB大学注册我们免费的MongoDB 3.6培训

From: https://www.sitepoint.com/json-schema-validation-expressive-query-syntax-in-mongodb-3-6/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值