如何在MongoDB中使用模式验证

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。

介绍

关系数据库 的一个重要方面——将数据库存储在由行和列组成的表中——是它们在具有已知数据类型字段的固定、刚性模式上运行。 像 MongoDB 这样的面向文档的数据库在这方面更加灵活,因为它们允许您根据需要重塑文档的结构。

但是,在某些情况下,您可能需要数据文档遵循特定结构或满足某些要求。 许多文档数据库允许您定义规则,这些规则指示文档数据的各个部分应如何构建,同时仍提供一些在需要时更改此结构的自由。

MongoDB 有一个称为模式验证的功能,它允许您对文档的结构应用约束。 Schema 验证是围绕 JSON Schema 构建的,它是 JSON 文档结构描述和验证的开放标准。 在本教程中,您将编写并应用验证规则来控制示例 MongoDB 集合中的文档结构。

先决条件

要遵循本教程,您将需要:

  • 具有 sudo 权限的常规非 root 用户和配置了 UFW 的防火墙的服务器。 本教程使用运行 Ubuntu 20.04 的服务器进行了验证,您可以按照此 Ubuntu 20.04 初始服务器设置教程来准备您的服务器。
  • MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。
  • 通过启用身份验证和创建管理用户来保护服务器的 MongoDB 实例。 要像这样保护 MongoDB,请按照我们关于 如何在 Ubuntu 20.04 上保护 MongoDB 的教程进行操作。
  • 熟悉查询 MongoDB 集合和过滤结果。 要了解如何使用 MongoDB 查询,请按照我们关于 如何在 MongoDB 中创建查询的指南进行操作。

注意:有关如何配置服务器、安装 MongoDB 和保护 MongoDB 安装的链接教程参考 Ubuntu 20.04。 本教程专注于 MongoDB 本身,而不是底层操作系统。 只要启用了身份验证,它通常适用于任何 MongoDB 安装,无论操作系统如何。


第 1 步 — 在不应用架构验证的情况下插入文档

为了突出 MongoDB 的模式验证功能以及它们为何有用,此步骤概述了如何打开 MongoDB shell 以连接到本地安装的 MongoDB 实例并在其中创建示例集合。 然后,通过在这个集合中插入一些示例文档,这一步将展示 MongoDB 在默认情况下如何不强制执行任何模式验证。 在后面的步骤中,您将开始自己创建和执行此类规则。

要创建本指南中使用的示例集合,请以您的管理用户身份连接到 MongoDB shell。 本教程遵循先决条件 MongoDB 安全教程 的约定,并假设此管理用户名为 AdminSammy,其身份验证数据库为 admin。 如果不同,请务必在以下命令中更改这些详细信息以反映您自己的设置:

mongo -u AdminSammy -p --authenticationDatabase admin

输入安装期间设置的密码以访问 shell。 输入密码后,您会看到 > 提示符。

为了说明模式验证功能,本指南的示例使用了一个示例数据库,其中包含代表世界上最高山脉的文档。 珠穆朗玛峰的样本文件将采用以下形式:

珠穆朗玛峰文件

{
    "name": "Everest",
    "height": 8848,
    "location": ["Nepal", "China"],
    "ascents": {
        "first": {
            "year": 1953,
        },
        "first_winter": {
            "year": 1980,
        },
        "total": 5656,
    }
}

本文档包含以下信息:

  • name:峰名。
  • height:峰高,单位为米。
  • location:山所在的国家。 此字段将值存储为数组,以允许位于多个国家/地区的山脉。
  • ascents:该字段的值是另一个文档。 当一个文档像这样存储在另一个文档中时,它被称为 嵌入 或 嵌套 文档。 每个 ascents 文档都描述了给定山峰的成功攀登。 具体来说,每个 ascents 文档都包含一个 total 字段,其中列出了每个给定峰的成功上升总数。 此外,这些嵌套文档中的每一个都包含两个字段,其值也是嵌套文档: first:该字段的值是一个嵌套文档,其中包含一个字段 year,它描述了第一次整体成功上升的年份。 first_winter:该字段的值是一个嵌套文档,其中还包含一个 year 字段,其值表示给定山的第一次成功冬季攀登的年份。

运行以下 insertOne() 方法,在您的 MongoDB 安装中同时创建一个名为 peaks 的集合,并将前面代表珠穆朗玛峰的示例文档插入其中:

db.peaks.insertOne(
    {
        "name": "Everest",
        "height": 8848,
        "location": ["Nepal", "China"],
        "ascents": {
            "first": {
                "year": 1953
            },
            "first_winter": {
                "year": 1980
            },
            "total": 5656
        }
    }
)

输出将包含成功消息和分配给新插入对象的对象标识符:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("618ffa70bfa69c93a8980443")
}

尽管您通过运行提供的 insertOne() 方法插入了此文档,但您在设计此文档的结构时拥有完全的自由。 在某些情况下,您可能希望在数据库中的文档结构方面具有一定程度的灵活性。 但是,您可能还希望确保文档结构的某些方面保持一致,以便更轻松地进行数据分析或处理。

为了说明为什么这很重要,请考虑一些可能输入到该数据库中的其他示例文档。

以下文档几乎与上一个代表珠穆朗玛峰的文档相同,但它不包含 name 字段:

没有名字的山

{
    "height": 8611,
    "location": ["Pakistan", "China"],
    "ascents": {
        "first": {
            "year": 1954
        },
        "first_winter": {
            "year": 1921
        },
        "total": 306
    }
}

对于包含世界上最高山脉列表的数据库,添加一个代表一座山但不包括其名称的文档可能是一个严重的错误。

在下一个示例文档中,存在山的名称,但它的高度表示为字符串而不是数字。 此外,该位置不是数组,而是单个值,并且没有关于上升尝试总数的信息:

山,其高度为字符串值

{
    "name": "Manaslu",
    "height": "8163m",
    "location": "Nepal"
}

解释与此示例一样多的遗漏的文档可能会很困难。 例如,如果 height 属性值在文档之间存储为不同的数据类型,您将无法成功按峰值高度对集合进行排序。

现在运行下面的 insertMany() 方法来测试这些文档是否可以插入到数据库中而不会导致任何错误:

db.peaks.insertMany([
    {
        "height": 8611,
        "location": ["Pakistan", "China"],
        "ascents": {
            "first": {
                "year": 1954
            },
            "first_winter": {
                "year": 1921
            },
            "total": 306
        }
    },
    {
        "name": "Manaslu",
        "height": "8163m",
        "location": "Nepal"
    }
])

事实证明,MongoDB 不会返回任何错误,并且两个文档都将成功插入:

Output{
        "acknowledged" : true,
        "insertedIds" : [
                ObjectId("618ffd0bbfa69c93a8980444"),
                ObjectId("618ffd0bbfa69c93a8980445")
        ]
}

正如此输出所示,这两个文档都是有效的 JSON,足以将它们插入到集合中。 但是,这还不足以使数据库保持逻辑一致和有意义。 在接下来的步骤中,您将构建模式验证规则以确保 peaks 集合中的数据文档遵循一些基本要求。

第 2 步 - 验证字符串字段

在 MongoDB 中,模式验证通过将 JSON 模式文档 分配给集合来处理单个集合。 JSON Schema 是一个开放标准,允许您定义和验证 JSON 文档的结构。 您可以通过创建一个模式定义来做到这一点,该定义列出了一组要求,给定集合中的文档必须遵循这些要求才能被视为有效。

任何给定的集合只能使用单个 JSON 模式,但您可以在创建集合时或之后的任何时间分配模式。 如果您决定稍后更改原始验证规则,则必须将原始 JSON Schema 文档替换为符合您的新要求的文档。

要将 JSON Schema 验证器文档分配给您在上一步中创建的 peaks 集合,您可以运行以下命令:

db.runCommand({
    "collMod": "collection_name",
    "validator": {
        $jsonSchema: {JSON_Schema_document}
    }
})

runCommand 方法执行 collMod 命令,该命令通过应用 validator 属性来修改指定的集合。 validator 属性负责模式验证,在此示例语法中,它接受 $jsonSchema 运算符。 此运算符定义一个 JSON Schema 文档,该文档将用作给定集合的模式验证器。

Warning:为了执行 collMod 命令,您的 MongoDB 用户必须被授予适当的权限。 假设您遵循 How To Secure Ubuntu on Ubuntu 20.04 上的先决条件教程,并以您在该指南中创建的管理用户身份连接到您的 MongoDB 实例,您将需要授予它一个额外的角色以跟随本指南中的示例。

首先,切换到用户的身份验证数据库。 这是以下示例中的 admin,但如果不同,则连接到您自己用户的身份验证数据库:

use admin
Outputswitched to db admin

然后运行 grantRolesToUser() 方法,并在创建 peaks 集合的数据库上授予用户 dbAdmin 角色。 以下示例假设 peaks 集合在 test 数据库中:

db.grantRolesToUser(
  "AdminSammy",
  [ { role : "dbAdmin", db : "test" } ]
  )

或者,您可以授予您的用户 dbAdminAnyDatabase 角色。 正如这个角色的名字所暗示的那样,它将授予您的用户 dbAdmin 对 MongoDB 实例上的每个数据库的权限:

db.grantRolesToUser(
  "AdminSammy",
  [ "dbAdminAnyDatabase" ]
  )

授予用户适当的角色后,导航回存储 peaks 集合的数据库:

use test
Outputswitched to db test

请注意,您还可以在创建集合时分配 JSON 模式验证器。 为此,您可以使用以下语法:

db.createCollection(
    "collection_name", {
    "validator": {
        $jsonSchema: {JSON_Schema_document}
    }
})

与前面的示例不同,此语法不包括 collMod 命令,因为集合还不存在,因此无法修改。 但是,与前面的示例一样,collection_name 是要为其分配验证器文档的集合的名称,而 validator 选项将指定的 JSON Schema 文档分配为集合的验证器。

像这样从一开始就应用 JSON Schema 验证器意味着您添加到集合中的每个文档都必须满足验证器设置的要求。 但是,当您将验证规则添加到现有集合时,新规则不会影响现有文档,直到您尝试修改它们。

您传递给 validator 属性的 JSON 模式文档应概述您要应用于集合的每个验证规则。 以下示例 JSON Schema 将确保 name 字段存在于集合中的每个文档中,并且 name 字段的值始终是字符串:

您的第一个 JSON Schema 文档验证名称字段

{
    "bsonType": "object",
    "description": "Document describing a mountain peak",
    "required": ["name"],
    "properties": {
        "name": {
            "bsonType": "string",
            "description": "Name must be a string and is required"
        }
    },
}

此模式文档概述了输入到集合中的文档的某些部分必须遵循的某些要求。 JSON Schema 文档的根部分(properties 之前的字段,在本例中为 bsonType、description 和 required)描述了数据库文档本身。

bsonType 属性描述了验证引擎期望找到的数据类型。 对于数据库文档本身,预期的类型是 object。 这意味着您只能将对象(换句话说,用大括号括起来的完整、有效的 JSON 文档({ 和 }))添加到此集合中。 如果您尝试插入其他类型的数据类型(如独立字符串、整数或数组),则会导致错误。

在 MongoDB 中,每个文档都是一个对象。 但是,JSON Schema 是一种用于描述和验证各种有效 JSON 文档的标准,普通数组或字符串也是有效的 JSON。 使用 MongoDB 模式验证时,您会发现必须始终在 JSON 模式验证器中将根文档的 bsonType 值设置为 object。

接下来,description 属性提供了在这个集合中找到的文档的简短描述。 此字段不是必需的,但除了用于验证文档之外,JSON Schemas 还可以用于注释文档的结构。 这可以帮助其他用户了解文档的用途,因此包含 description 字段可能是一个很好的做法。

验证文档中的下一个属性是 required 字段。 required 字段只能接受包含文档字段列表的数组,这些文档字段必须存在于集合中的每个文档中。 在此示例中,["name"] 表示文档只需包含 name 字段即可被视为有效。

接下来是一个 properties 对象,它描述了用于验证文档字段的规则。 对于您要为其定义规则的每个字段,请包含一个以该字段命名的嵌入式 JSON 架构文档。 请注意,您可以为 required 数组中未列出的字段定义架构规则。 这在您的数据包含不需要的字段但您仍然希望它们在存在时遵循某些规则的情况下很有用。

这些嵌入的模式文档将遵循与主文档类似的语法。 在此示例中,bsonType 属性将要求每个文档的 name 字段为 string。 该嵌入文档还包含一个简短的 description 字段。

要将此 JSON Schema 应用于您在上一步中创建的 peaks 集合,请运行以下 runCommand() 方法:

db.runCommand({
    "collMod": "peaks",
    "validator": {
        $jsonSchema: {
            "bsonType": "object",
            "description": "Document describing a mountain peak",
            "required": ["name"],
            "properties": {
                "name": {
                    "bsonType": "string",
                    "description": "Name must be a string and is required"
                }
            },
        }
    }
})

MongoDB 将响应一条成功消息,指示该集合已成功修改:

Output{ "ok" : 1 }

之后,如果文档没有 name 字段,MongoDB 将不再允许您将文档插入到 peaks 集合中。 要对此进行测试,请尝试插入您在上一步中插入的完整描述一座山的文档,除了缺少 name 字段:

db.peaks.insertOne(
    {
        "height": 8611,
        "location": ["Pakistan", "China"],
        "ascents": {
            "first": {
                "year": 1954
            },
            "first_winter": {
                "year": 1921
            },
            "total": 306
        }
    }
)

这次,该操作将触发一条错误消息,指示文档验证失败:

OutputWriteError({
        "index" : 0,
        "code" : 121,
        "errmsg" : "Document failed validation",
        . . .
})

MongoDB 不会插入任何未能通过 JSON Schema 中指定的验证规则的文档。

注意: 从 MongoDB 5.0 开始,当验证失败时,错误消息指向失败的约束。 在 MongoDB 4.4 及更早版本中,数据库未提供有关失败原因的更多详细信息。


您还可以通过运行以下 insertOne() 方法来测试 MongoDB 是否会强制执行您包含在 JSON Schema 中的数据类型要求。 这与上一个操作类似,但这次它包含一个 name 字段。 但是,此字段的值是数字而不是字符串:

db.peaks.insertOne(
    {
        "name": 123,
        "height": 8611,
        "location": ["Pakistan", "China"],
        "ascents": {
            "first": {
                "year": 1954
            },
            "first_winter": {
                "year": 1921
            },
            "total": 306
        }
    }
)

再次验证将失败。 即使存在 name 字段,它也不符合要求它是字符串的约束:

OutputWriteError({
        "index" : 0,
        "code" : 121,
        "errmsg" : "Document failed validation",
        . . .
})

再试一次,但文档中存在 name 字段,并且后跟一个字符串值。 这一次,name 是文档中唯一的字段:

db.peaks.insertOne(
    {
        "name": "K2"
    }
)

操作将成功,文档将照常接收对象标识符:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("61900965bfa69c93a8980447")
}

架构验证规则仅适用于 name 字段。 此时,只要 name 字段满足验证要求,文档就会正确插入。 文档的其余部分可以采用任何形状。

这样,您就创建了第一个 JSON Schema 文档并将第一个模式验证规则应用于 name 字段,要求它存在并且是一个字符串。 但是,对于不同的数据类型有不同的验证选项。 接下来,您将验证存储在每个文档的 height 字段中的数值。

第 3 步 - 验证数字字段

当您将以下文档插入 peaks 集合时,请回想第 1 步:

山,其高度为字符串值

{
    "name": "Manaslu",
    "height": "8163m",
    "location": "Nepal"
}

尽管此文档的 height 值是字符串而不是数字,但您用于插入此文档的 insertMany() 方法是成功的。 这是可能的,因为您尚未为 height 字段添加任何验证规则。

MongoDB 将接受该字段的任何值——即使是对该字段没有任何意义的值,例如负值——只要插入的文档是用有效的 JSON 语法编写的。 要解决此问题,您可以扩展上一步的模式验证文档,以包含有关 height 字段的其他规则。

首先确保 height 字段始终存在于新插入的文档中,并且始终表示为数字。 使用以下命令修改架构验证:

db.runCommand({
    "collMod": "peaks",
    "validator": {
        $jsonSchema: {
            "bsonType": "object",
            "description": "Document describing a mountain peak",
            "required": ["name", "height"],
            "properties": {
                "name": {
                    "bsonType": "string",
                    "description": "Name must be a string and is required"
                },
                "height": {
                    "bsonType": "number",
                    "description": "Height must be a number and is required"
                }
            },
        }
    }
})

在此命令的架构文档中,height 字段包含在 required 数组中。 同样,在 properties 对象中有一个 height 文档,它要求任何新的 height 值都是 number。 同样,description 字段是辅助的,您包含的任何描述都应该只是为了帮助其他用户理解 JSON Schema 背后的意图。

MongoDB 将响应一条简短的成功消息,让您知道该集合已成功修改:

Output{ "ok" : 1 }

现在您可以测试新规则了。 尝试插入具有通过验证文档所需的最小文档结构的文档。 以下方法将插入一个包含仅有的两个必填字段 name 和 height 的文档:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300
    }
)

插入将成功:

Output{
  acknowledged: true,
  insertedId: ObjectId("61e0c8c376b24e08f998e371")
}

接下来,尝试插入一个缺少 height 字段的文档:

db.peaks.insertOne(
    {
        "name": "Test peak"
    }
)

然后尝试另一个包含 height 字段,但该字段包含字符串值:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": "8300m"
    }
)

两次,操作都会触发错误消息并失败:

OutputWriteError({
        "index" : 0,
        "code" : 121,
        "errmsg" : "Document failed validation",
        . . .
})

但是,如果您尝试插入负高度的山峰,山峰将正确保存:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": -100
    }
)

为了防止这种情况,您可以向模式验证文档添加更多属性。 通过运行以下操作替换当前架构验证设置:

db.runCommand({
    "collMod": "peaks",
    "validator": {
        $jsonSchema: {
            "bsonType": "object",
            "description": "Document describing a mountain peak",
            "required": ["name", "height"],
            "properties": {
                "name": {
                    "bsonType": "string",
                    "description": "Name must be a string and is required"
                },
                "height": {
                    "bsonType": "number",
                    "description": "Height must be a number between 100 and 10000 and is required",
                    "minimum": 100,
                    "maximum": 10000
                }
            },
        }
    }
})

新的 minimum 和 maximum 属性对 height 字段中包含的值设置约束,确保它们不能低于 100 或高于 10000。 在这种情况下,此范围是有意义的,因为此集合用于存储有关山峰高度的信息,但您可以为这些属性选择您喜欢的任何值。

现在,如果您再次尝试插入具有负 height 值的峰值,操作将失败:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": -100
    }
)
OutputWriteError({
    "index" : 0,
    "code" : 121,
    "errmsg" : "Document failed validation",
. . .

如此输出所示,您的文档架构现在验证每个文档的 name 字段中保存的字符串值以及 height 字段中保存的数值。 继续阅读以了解如何验证存储在每个文档的 location 字段中的数组值。

第 4 步 - 验证数组字段

现在每个峰的 name 和 height 值正在通过模式验证约束进行验证,我们可以将注意力转向 location 字段以保证其数据一致性。

指定山脉的位置比人们想象的要棘手,因为山峰跨越一个以上的国家,许多著名的八千山就是这种情况。 因此,将每个峰的 location 数据存储为包含一个或多个国家/地区名称的数组而不是字符串值是有意义的。 与 height 值一样,确保每个 location 字段的数据类型在每个文档中都是一致的,这有助于在使用聚合管道时汇总数据。

首先,考虑一些用户可能输入的 location 值示例,并权衡哪些是有效的或无效的:

  • ["Nepal", "China"]:这是一个二元素数组,对于跨越两个国家的山脉来说是一个有效值。
  • ["Nepal"]:这个例子是一个单元素数组,对于位于单个国家的一座山来说,它也是一个有效值。
  • "Nepal":这个例子是一个纯字符串。 这将是无效的,因为尽管它列出了一个国家/地区,但 location 字段应始终包含一个数组
  • []:一个空数组,这个例子不会是一个有效值。 毕竟,每座山都必须存在于至少一个国家。
  • ["Nepal", "Nepal"]:这个二元素数组也是无效的,因为它包含两次出现的相同值。
  • ["Nepal", 15]:最后,这个二元素数组是无效的,因为它的值之一是数字而不是字符串,这不是正确的位置名称。

为确保 MongoDB 将这些示例正确解释为有效或无效,请运行以下操作为 peaks 集合创建一些新的验证规则:

db.runCommand({
    "collMod": "peaks",
    "validator": {
        $jsonSchema: {
            "bsonType": "object",
            "description": "Document describing a mountain peak",
            "required": ["name", "height", "location"],
            "properties": {
                "name": {
                    "bsonType": "string",
                    "description": "Name must be a string and is required"
                },
                "height": {
                    "bsonType": "number",
                    "description": "Height must be a number between 100 and 10000 and is required",
                    "minimum": 100,
                    "maximum": 10000
                },
                "location": {
                    "bsonType": "array",
                    "description": "Location must be an array of strings",
                    "minItems": 1,
                    "uniqueItems": true,
                    "items": {
                        "bsonType": "string"
                    }
                }
            },
        }
    }
})

在此 $jsonSchema 对象中,location 字段包含在 required 数组以及 properties 对象中。 在那里,它使用 array 的 bsonType 定义,以确保 location 值始终是一个数组,而不是单个字符串或数字。

minItems 属性验证数组必须包含至少一个元素,并且 uniqueItems 属性设置为 true 以确保每个 location 数组中的元素将是独一无二的。 这将阻止像 ["Nepal", "Nepal"] 这样的值被接受。 最后,items 子文档为每个单独的数组项定义了验证模式。 在这里,唯一的期望是 location 数组中的每个项目都必须是字符串。

注意: 每个 bsonType 的可用架构文档属性都不同,并且根据字段类型,您将能够验证字段值的不同方面。 例如,使用 number 值,您可以定义最小和最大允许值以创建可接受值的范围。 在前面的示例中,通过将 location 字段的 bsonType 设置为 array,您可以验证特定于数组的特征。

您可以在 JSON 模式文档 中找到所有可能的验证选项的详细信息。


执行命令后,MongoDB 将响应一条简短的成功消息,表明已使用新的模式文档成功修改了集合:

Output{ "ok" : 1 }

现在尝试插入与之前准备的示例匹配的文档,以测试新规则的行为方式。 再一次,让我们使用最小的文档结构,只有 name、height 和 location 字段存在。

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": ["Nepal", "China"]
    }
)

文档将成功插入,因为它满足所有定义的验证期望。 同样,以下文档将插入而不会出错:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": ["Nepal"]
    }
)

但是,如果您要运行以下任何 insertOne() 方法,它们将触发验证错误并失败:

db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": "Nepal"
    }
)
db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": []
    }
)
db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": ["Nepal", "Nepal"]
    }
)
db.peaks.insertOne(
    {
        "name": "Test peak",
        "height": 8300,
        "location": ["Nepal", 15]
    }
)

根据您之前定义的验证规则,这些操作中提供的 location 值被视为无效。

完成此步骤后,描述山顶的三个主要字段已通过 MongoDB 的模式验证功能进行验证。 在下一步中,您将学习如何使用 ascents 字段作为示例来验证嵌套文档。

第 5 步 - 验证嵌入式文档

此时,您的 peaks 集合具有三个字段 - name、height 和 location - 正在通过模式验证进行检查。 此步骤侧重于为 ascents 字段定义验证规则,该字段描述了登顶每个峰的成功尝试。

在步骤 1 中代表珠穆朗玛峰的示例文档中,ascents 字段的结构如下:

珠穆朗玛峰文件

{
    "name": "Everest",
    "height": 8848,
    "location": ["Nepal", "China"],
    "ascents": {
        "first": {
            "year": 1953,
        },
        "first_winter": {
            "year": 1980,
        },
        "total": 5656,
    }
}

ascents 子文档包含一个 total 字段,其值表示给定山峰的攀登尝试总数。 它还包含有关这座山的首次冬季攀登以及整体首次攀登的信息。 然而,这些对于山的描述可能不是必不可少的。 毕竟,有些山峰可能还没有在冬季登顶,或者登顶日期存在争议或未知。 现在,假设您总是希望在每个文档中拥有的信息是上升尝试的总数。

您可以更改架构验证文档,以便 ascents 字段必须始终存在并且其值必须始终是子文档。 反过来,此子文档必须始终包含一个 total 属性,该属性包含一个大于或等于零的数字。 本指南不需要 first 和 first_winter 字段,因此验证表单不会考虑它们,它们可以采用灵活的形式。

再次,通过运行以下 runCommand() 方法替换 peaks 集合的模式验证文档:

db.runCommand({
    "collMod": "peaks",
    "validator": {
        $jsonSchema: {
            "bsonType": "object",
            "description": "Document describing a mountain peak",
            "required": ["name", "height", "location", "ascents"],
            "properties": {
                "name": {
                    "bsonType": "string",
                    "description": "Name must be a string and is required"
                },
                "height": {
                    "bsonType": "number",
                    "description": "Height must be a number between 100 and 10000 and is required",
                    "minimum": 100,
                    "maximum": 10000
                },
                "location": {
                    "bsonType": "array",
                    "description": "Location must be an array of strings",
                    "minItems": 1,
                    "uniqueItems": true,
                    "items": {
                        "bsonType": "string"
                    }
                },
                "ascents": {
                    "bsonType": "object",
                    "description": "Ascent attempts information",
                    "required": ["total"],
                    "properties": {
                        "total": {
                            "bsonType": "number",
                            "description": "Total number of ascents must be 0 or higher",
                            "minimum": 0
                        }
                   }
                }
            },
        }
    }
})

每当文档在其任何字段下包含子文档时,该字段的 JSON 模式都遵循与主文档模式完全相同的语法。 就像相同的文档可以相互嵌套一样,验证模式也将它们相互嵌套。 这使得为包含分层结构中的多个子文档的文档结构定义复杂的验证模式变得很简单。

在此 JSON Schema 文档中,ascents 字段包含在 required 数组中,因此它是强制性的。 它也出现在 properties 对象中,它由 object 的 bsonType 定义,就像根文档本身一样。

请注意,ascents 验证的定义遵循与根文档类似的原则。 它有 required 字段,表示子文档必须包含的属性。 它还定义了一个 properties 列表,遵循相同的结构。 由于 ascents 字段是一个子文档,它的值将像较大文档的值一样被验证。

在 ascents 中,有一个 required 数组,其唯一值是 total,这意味着每个 ascents 子文档都需要包含一个 total场地。 此后,total 值在 properties 对象中进行了详细描述,该对象指定它必须始终是 number 且 minimum 值为零。

同样,由于 first 和 first_winter 字段对于本指南来说都不是强制性的,因此它们不包含在这些验证规则中。

应用此架构验证文档后,尝试插入第一步中的示例 Mount Everest 文档,以验证它是否允许您插入已确定为有效的文档:

db.peaks.insertOne(
    {
        "name": "Everest",
        "height": 8848,
        "location": ["Nepal", "China"],
        "ascents": {
            "first": {
                "year": 1953,
            },
            "first_winter": {
                "year": 1980,
            },
            "total": 5656,
        }
    }
)

文档保存成功,MongoDB 返回新的对象标识符:

Output{
        "acknowledged" : true,
        "insertedId" : ObjectId("619100f51292cb2faee531f8")
}

为确保最后的验证工作正常,请尝试插入不包含 ascents 字段的文档:

db.peaks.insertOne(
    {
        "name": "Everest",
        "height": 8848,
        "location": ["Nepal", "China"]
    }
)

这次,该操作将触发一条错误消息,指出文档验证失败:

OutputWriteError({
        "index" : 0,
        "code" : 121,
        "errmsg" : "Document failed validation",
        . . .
})

现在尝试插入 ascents 子文档缺少 total 字段的文档:

db.peaks.insertOne(
    {
        "name": "Everest",
        "height": 8848,
        "location": ["Nepal", "China"],
        "ascents": {
            "first": {
                "year": 1953,
            },
            "first_winter": {
                "year": 1980,
            }
        }
    }
)

这将再次触发错误。

作为最终测试,尝试输入包含 ascents 字段和 total 值的文档,但该值为负:

db.peaks.insertOne(
    {
        "name": "Everest",
        "height": 8848,
        "location": ["Nepal", "China"],
        "ascents": {
            "first": {
                "year": 1953,
            },
            "first_winter": {
                "year": 1980,
            },
            "total": -100
        }
    }
)

由于 total 值为负,此文档也将无法通过验证测试。

结论

通过学习本教程,您熟悉了 JSON Schema 文档以及如何在将它们保存到集合之前使用它们来验证文档结构。 然后,您使用 JSON Schema 文档来验证字段类型并将值约束应用于数字和数组。 您还学习了如何验证嵌套文档结构中的子文档。

MongoDB 的模式验证功能不应被视为应用程序级别数据验证的替代品,但它可以进一步防止违反对保持数据有意义至关重要的数据约束。 使用模式验证可以成为结构化数据的有用工具,同时保留无模式数据存储方法的灵活性。 使用模式验证,您可以完全控制要验证的文档结构的那些部分以及那些您希望保持开放式的部分。

本教程仅描述了 MongoDB 模式验证功能的一个子集。 您可以对不同的 MongoDB 数据类型应用更多约束,甚至可以更改验证行为的严格性并使用 JSON Schema 过滤和验证现有文档。 我们鼓励您学习官方 官方 MongoDB 文档 以了解有关模式验证的更多信息以及它如何帮助您处理存储在数据库中的数据。