如何在MongoDB中执行CRUD操作
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
MongoDB是一个持久化的面向文档的数据库,用于以文档的形式存储和处理数据。 与其他数据库管理系统一样,MongoDB 允许您通过四种基本类型的数据操作来管理数据并与之交互:
- Create 操作,涉及将数据写入数据库
- R读取操作,查询数据库以从中检索数据
- U更新操作,更改数据库中已存在的数据
- Delete 操作,从数据库中永久删除数据
这四个操作合称为 CRUD 操作。
本教程概述了如何创建新的 MongoDB 文档,然后检索它们以读取其数据。 它还解释了如何更新文档中的数据,以及如何在不再需要文档时删除它们。
先决条件
要遵循本教程,您将需要:
- 具有
sudo
权限的常规非 root 用户和配置了 UFW 的防火墙的服务器。 本教程使用运行 Ubuntu 20.04 的服务器进行了验证,您可以按照此 Ubuntu 20.04 初始服务器设置教程来准备您的服务器。 - MongoDB 安装在您的服务器上。 要进行此设置,请按照我们关于 如何在 Ubuntu 20.04 上安装 MongoDB 的教程进行操作。
- 通过启用身份验证和创建管理用户来保护服务器的 MongoDB 实例。 要像这样保护 MongoDB,请按照我们关于 如何在 Ubuntu 20.04 上保护 MongoDB 的教程进行操作。
- 基本熟悉本教程中使用的 MongoDB shell。 要了解如何使用 MongoDB shell,请按照教程 如何使用 MongoDB Shell。
注意:有关如何配置服务器、安装和安全 MongoDB 安装的链接教程参考 Ubuntu 20.04。 本教程专注于 MongoDB 本身,而不是底层操作系统。 只要启用了身份验证,它通常适用于任何 MongoDB 安装,无论操作系统如何。
第 1 步 — 连接到 MongoDB 服务器
本指南涉及使用 MongoDB shell 与 MongoDB 交互。 为了在 MongoDB 中跟随并练习 CRUD 操作,您必须首先通过打开 MongoDB shell 连接到 MongoDB 数据库。
如果您的 MongoDB 实例在远程服务器上运行,请从本地计算机 SSH 到该服务器:
ssh sammy@your_server_ip
然后通过打开 MongoDB shell 连接到您的 MongoDB 安装。 确保以具有写入和读取数据权限的 MongoDB 用户身份连接。 如果您遵循 先决条件 MongoDB 安全教程 ,您可以作为您在该指南的 Step 1 中创建的管理用户进行连接:
mongo -u AdminSammy -p --authenticationDatabase admin
提供用户密码后,您的终端提示符将变为大于号 (>
)。 这意味着 shell 现在已准备好接受它所连接的 MongoDB 服务器的命令。
注意: 在新连接时,MongoDB shell 默认会自动连接到 test
数据库。 您可以安全地使用此数据库来试验 MongoDB 和 MongoDB shell。
或者,您也可以切换到另一个数据库来运行本教程中给出的所有示例命令。 要切换到另一个数据库,请运行 use
命令,后跟数据库名称:
use database_name
现在您已经使用 MongoDB shell 连接到 MongoDB 服务器,您可以继续创建新文档。
第 2 步 — 创建文档
为了让您可以在本指南的后续步骤中练习读取、更新和删除数据,本步骤重点介绍如何在 MongoDB 中创建数据文档。
想象一下,您正在使用 MongoDB 构建和管理来自世界各地的著名历史古迹的目录。 该目录将存储每个纪念碑的名称、国家、城市和地理位置等信息。
此目录中的文档将遵循类似于此示例的格式,表示 The Pyramids of Giza:
吉萨金字塔
{ "name": "The Pyramids of Giza", "city": "Giza", "country": "Egypt", "gps": { "lat": 29.976480, "lng": 31.131302 } }
与所有 MongoDB 文档一样,此文档是用 BSON 编写的。 BSON 是 JSON 的二进制形式,一种人类可读的数据格式。 BSON 或 JSON 文档中的所有数据都表示为采用 field: value
形式的字段-值对。
本文档由四个字段组成。 首先是纪念碑的名称,其次是城市和国家。 所有这三个字段都包含字符串。 最后一个字段,称为 gps
,是一个嵌套文档,详细说明了纪念碑的 GPS 位置。 该位置由一对纬度和经度坐标组成,分别由 lat
和 lng
字段表示,每个字段都保存浮点值。
注意: 您可以在我们的概念文章 An Introduction to Document-Oriented Databases 中了解有关 MongoDB 文档结构的更多信息。
使用 insertOne
方法将此文档插入名为 monuments
的新集合中。 顾名思义,insertOne
用于创建单个文档,而不是一次创建多个文档。
在 MongoDB shell 中,运行以下操作:
db.monuments.insertOne( { "name": "The Pyramids of Giza", "city": "Giza", "country": "Egypt", "gps": { "lat": 29.976480, "lng": 31.131302 } } )
请注意,在执行此 insertOne
方法之前,您尚未显式创建 monuments
集合。 MongoDB 允许您在不存在的集合上自由运行命令,并且只有在插入第一个对象时才会创建缺少的集合。 通过执行这个示例 insertOne()
方法,它不仅会将文档插入到集合中,还会自动创建集合。
MongoDB 将执行 insertOne
方法并插入代表吉萨金字塔的请求文档。 该操作的输出将通知您它已成功执行,并且还提供了它为新文档自动生成的 ObjectId
:
Output{ "acknowledged" : true, "insertedId" : ObjectId("6105752352e6d1ebb7072647") }
在 MongoDB 中,集合中的每个文档都必须有一个唯一的 _id
字段作为主键。 您可以包含 _id
字段并为其提供您自己选择的值,只要您确保每个文档的 _id
字段是唯一的。 但是,如果新文档省略了 _id
字段,MongoDB 将自动生成一个对象标识符(以 ObjectId
对象的形式)作为 _id
字段的值。
您可以通过检查 monuments
集合中的对象计数来验证文档是否已插入:
db.monuments.count()
由于您只在此集合中插入了一个文档,因此 count
方法将返回 1
:
Output1
如果您想创建多个文档,像这样一个一个地插入文档很快就会变得乏味。 MongoDB 提供了 insertMany
方法,您可以使用该方法在单个操作中插入多个文档。
运行以下示例命令,该命令使用 insertMany
方法将另外六个著名古迹插入到 monuments
集合中:
db.monuments.insertMany([ {"name": "The Valley of the Kings", "city": "Luxor", "country": "Egypt", "gps": { "lat": 25.746424, "lng": 32.605309 }}, {"name": "Arc de Triomphe", "city": "Paris", "country": "France", "gps": { "lat": 48.873756, "lng": 2.294946 }}, {"name": "The Eiffel Tower", "city": "Paris", "country": "France", "gps": { "lat": 48.858093, "lng": 2.294694 }}, {"name": "Acropolis", "city": "Athens", "country": "Greece", "gps": { "lat": 37.970833, "lng": 23.726110 }}, {"name": "The Great Wall of China", "city": "Huairou", "country": "China", "gps": { "lat": 40.431908, "lng": 116.570374 }}, {"name": "The Statue of Liberty", "city": "New York", "country": "USA", "gps": { "lat": 40.689247, "lng": -74.044502 }} ])
请注意围绕六个文档的方括号([
和 ]
)。 这些括号表示文档的 array。 在方括号内,多个对象可以一个接一个地出现,用逗号分隔。 在 MongoDB 方法需要多个对象的情况下,您可以像这样以数组的形式提供对象列表。
MongoDB 将响应几个对象标识符,每个新插入的对象一个:
Output{ "acknowledged" : true, "insertedIds" : [ ObjectId("6105770952e6d1ebb7072648"), ObjectId("6105770952e6d1ebb7072649"), ObjectId("6105770952e6d1ebb707264a"), ObjectId("6105770952e6d1ebb707264b"), ObjectId("6105770952e6d1ebb707264c"), ObjectId("6105770952e6d1ebb707264d") ] }
您可以通过检查 monuments
集合中的对象计数来验证文档是否已插入:
db.monuments.count()
添加这六个新文档后,此命令的预期输出为 7
:
Output7
这样,您就使用了两种单独的插入方法来创建代表几个著名纪念碑的大量文档。 接下来,您将使用 MongoDB 的 find()
方法读取刚刚插入的数据。
第三步——阅读文件
现在您的集合中存储了一些文档,您可以查询数据库以检索这些文档并读取它们的数据。 此步骤首先概述如何查询给定集合中的所有文档,然后描述如何使用过滤器缩小检索到的文档列表。
完成上一步后,您现在有七个描述著名古迹的文档插入到 monuments
集合中。 您可以使用 find()
方法通过一次操作检索所有七个文档:
db.monuments.find()
此方法在不使用任何参数时不应用任何过滤并要求 MongoDB 返回指定集合 monuments
中的所有可用对象。 MongoDB 将返回以下输出:
Output{ "_id" : ObjectId("6105752352e6d1ebb7072647"), "name" : "The Pyramids of Giza", "city" : "Giza", "country" : "Egypt", "gps" : { "lat" : 29.97648, "lng" : 31.131302 } } { "_id" : ObjectId("6105770952e6d1ebb7072648"), "name" : "The Valley of the Kings", "city" : "Luxor", "country" : "Egypt", "gps" : { "lat" : 25.746424, "lng" : 32.605309 } } { "_id" : ObjectId("6105770952e6d1ebb7072649"), "name" : "Arc de Triomphe", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.873756, "lng" : 2.294946 } } { "_id" : ObjectId("6105770952e6d1ebb707264a"), "name" : "The Eiffel Tower", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.858093, "lng" : 2.294694 } } { "_id" : ObjectId("6105770952e6d1ebb707264b"), "name" : "Acropolis", "city" : "Athens", "country" : "Greece", "gps" : { "lat" : 37.970833, "lng" : 23.72611 } } { "_id" : ObjectId("6105770952e6d1ebb707264c"), "name" : "The Great Wall of China", "city" : "Huairou", "country" : "China", "gps" : { "lat" : 40.431908, "lng" : 116.570374 } } { "_id" : ObjectId("6105770952e6d1ebb707264d"), "name" : "The Statue of Liberty", "city" : "New York", "country" : "USA", "gps" : { "lat" : 40.689247, "lng" : -74.044502 } }
MongoDB shell 将所有七个文档一一完整地打印出来。 请注意,这些对象中的每一个都有一个您没有定义的 _id
属性。 如前所述,_id
字段用作其各自文档的主键,并且在您在上一步中运行 insertMany
方法时自动创建。
MongoDB shell 的默认输出是紧凑的,每个文档的字段和值都打印在一行中。 特别是对于包含多个字段或嵌套文档的对象,这可能会变得难以阅读。
为了使 find()
方法的输出更具可读性,您可以使用它的 pretty
打印功能,如下所示:
db.monuments.find().pretty()
这一次,MongoDB shell 将打印多行文档,每行都有缩进:
Output{ "_id" : ObjectId("6105752352e6d1ebb7072647"), "name" : "The Pyramids of Giza", "city" : "Giza", "country" : "Egypt", "gps" : { "lat" : 29.97648, "lng" : 31.131302 } } { "_id" : ObjectId("6105770952e6d1ebb7072648"), "name" : "The Valley of the Kings", "city" : "Luxor", "country" : "Egypt", "gps" : { "lat" : 25.746424, "lng" : 32.605309 } } . . .
请注意,在前面的两个示例中,find()
方法在没有任何参数的情况下执行。 在这两种情况下,它都返回集合中的每个对象。 您可以将过滤器应用于查询以缩小结果范围。
回想一下前面的例子,MongoDB 自动为 The Valley of the Kings 分配了一个值为 ObjectId("6105770952e6d1ebb7072648")
的对象标识符。 对象标识符不仅仅是 ObjectId("")
中的十六进制字符串,而是整个 ObjectId
对象——MongoDB 中用于存储对象标识符的特殊数据类型。
以下 find()
方法通过接受 查询过滤器文档 作为参数返回单个对象。 查询过滤器文档遵循与插入到集合中的文档相同的结构,由字段和值组成,但它们用于过滤查询结果。
本例中使用的查询过滤文档包含_id
字段,以帝王谷的对象标识符为值。 要在您自己的数据库上运行此查询,请确保将突出显示的对象标识符替换为存储在您自己的 monuments
集合中的文档之一:
db.monuments.find({"_id": ObjectId("6105770952e6d1ebb7072648")}).pretty()
此示例中的查询过滤器文档使用相等条件,这意味着查询将返回具有与文档中指定的匹配的字段和值对的任何文档。 本质上,此示例告诉 find()
方法仅返回 _id
值等于 ObjectId("6105770952e6d1ebb7072648")
的文档。
执行此方法后,MongoDB 将返回与请求的对象标识符匹配的单个对象:
Output{ "_id" : ObjectId("6105770952e6d1ebb7072648"), "name" : "The Valley of the Kings", "city" : "Luxor", "country" : "Egypt", "gps" : { "lat" : 25.746424, "lng" : 32.605309 } }
您也可以在文档中的任何其他字段上使用质量条件。 为了说明,尝试搜索法国的纪念碑:
db.monuments.find({"country": "France"}).pretty()
此方法将返回两个纪念碑:
Output{ "_id" : ObjectId("6105770952e6d1ebb7072649"), "name" : "Arc de Triomphe", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.873756, "lng" : 2.294946 } } { "_id" : ObjectId("6105770952e6d1ebb707264a"), "name" : "The Eiffel Tower", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.858093, "lng" : 2.294694 } }
查询过滤器文档非常强大和灵活,它们允许您将复杂的过滤器应用于集合文档。
您可以在我们的 如何创建查询 教程中了解有关查询集合的不同方法的更多信息。
-->
第 4 步 — 更新文档
像 MongoDB 这样的面向文档的数据库中的文档随着时间的推移而变化是很常见的。 有时,它们的结构必须随着应用程序需求的变化而发展,否则数据本身可能会发生变化。 此步骤侧重于如何通过更改单个文档中的字段值以及向集合中的每个文档添加新字段来更新现有文档。
与 insertOne()
和 insertMany()
方法类似,MongoDB 提供了允许您一次更新单个文档或多个文档的方法。 这些更新方法的一个重要区别是,在创建新文档时,您只需将文档数据作为方法参数传递。 要更新集合中的现有文档,您还必须传递一个参数来指定要更新的文档。
为了允许用户执行此操作,MongoDB 在更新方法中使用与您在上一步中查找和检索文档相同的查询过滤文档机制。 任何可用于检索文档的查询过滤器文档也可用于指定要更新的文档。
尝试将 Arc de Triomphe 的名称更改为 Arc de Triomphe de l'Étoile 的全名。 为此,请使用更新单个文档的 updateOne()
方法:
db.monuments.updateOne( { "name": "Arc de Triomphe" }, { $set: { "name": "Arc de Triomphe de l'Étoile" } } )
updateOne
方法的第一个参数是具有单个相等条件的查询过滤器文档,如上一步所述。 在此示例中,{ "name": "Arc de Triomphe" }
查找具有 name
键并保存 Arc de Triomphe
值的文档。 任何有效的查询过滤器文档都可以在这里使用。
第二个参数是更新文档,指定在更新期间应应用哪些更改。 更新文档由作为键的更新运算符和作为值的每个运算符的参数组成。 在此示例中,使用的更新运算符是 $set
。 它负责将文档字段设置为新值,并需要一个带有新字段值的 JSON 对象。 这里,set: { "name": "Arc de Triomphe de l'Étoile" }
告诉 MongoDB 将字段 name
的值设置为 Arc de Triomphe de l'Étoile
。
该方法将返回一个结果,告诉您查询过滤器文档找到了一个对象,并且成功更新了一个对象。
Output{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
注意:如果文档查询过滤器不够精确,无法选择单个文档,updateOne()
将仅更新从多个结果返回的first文档。
要检查更新是否有效,请尝试检索与 France
相关的所有纪念碑:
db.monuments.find({"country": "France"}).pretty()
这一次,该方法返回 Arc de Triomphe 但它的全名被更新操作更改:
Output{ "_id" : ObjectId("6105770952e6d1ebb7072649"), "name" : "Arc de Triomphe de l'Étoile", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.873756, "lng" : 2.294946 } } . . .
要修改多个文档,您可以改用 updateMany()
方法。
例如,假设您注意到没有关于谁创建了该条目的信息,并且您想归功于将每个纪念碑添加到数据库的作者。 为此,您将向 monuments
集合中的每个文档添加一个新的 editor
字段。
以下示例包含一个空的查询过滤器文档。 通过包含一个空查询文档,此操作将匹配集合中的每个文档,并且 updateMany()
方法将影响每个文档。 更新文档为每个文档添加一个新的 editor
字段,并为其分配一个值 Sammy
:
db.monuments.updateMany( { }, { $set: { "editor": "Sammy" } } )
此方法将返回以下输出:
Output{ "acknowledged" : true, "matchedCount" : 7, "modifiedCount" : 7 }
此输出通知您匹配了七个文档,并且还修改了七个文档。
确认已应用更改:
db.monuments.find().pretty()
Output{ "_id" : ObjectId("6105752352e6d1ebb7072647"), "name" : "The Pyramids of Giza", "city" : "Giza", "country" : "Egypt", "gps" : { "lat" : 29.97648, "lng" : 31.131302 }, "editor" : "Sammy" } { "_id" : ObjectId("6105770952e6d1ebb7072648"), "name" : "The Valley of the Kings", "city" : "Luxor", "country" : "Egypt", "gps" : { "lat" : 25.746424, "lng" : 32.605309 }, "editor" : "Sammy" } . . .
所有返回的文档现在都有一个名为 editor
的新字段设置为 Sammy
。 通过向 $set
更新运算符提供一个不存在的字段名称,更新操作将在所有匹配的文档中创建缺失字段并正确设置新值。
尽管您可能最常使用 $set
,但 MongoDB 中提供了许多其他更新运算符,允许您对文档的数据和结构进行复杂的更改。 您可以在 MongoDB 的 官方文档 中了解有关这些更新运算符的更多信息。
第 5 步 — 删除文档
有时数据库中的数据已过时,需要删除。 和Mongo的更新和插入操作一样,有deleteOne()
方法,只删除查询过滤文档匹配的first文档,还有deleteMany()
,删除多个对象立刻。
要练习使用这些方法,首先尝试移除您之前修改的 Arc de Triomphe de l'Étoile 纪念碑:
db.monuments.deleteOne( { "name": "Arc de Triomphe de l'Étoile" } )
请注意,此方法包括一个查询过滤器文档,如前面的更新和检索示例。 和以前一样,您可以使用任何有效查询来指定将删除哪些文档。
MongoDB 将返回以下结果:
Output{ "acknowledged" : true, "deletedCount" : 1 }
在这里,结果会告诉您在此过程中删除了多少文档。
通过查询法国的纪念碑来检查文档是否确实已从集合中删除:
db.monuments.find({"country": "France"}).pretty()
这次该方法仅返回单个纪念碑 The Eiffel Tower,因为您删除了 Arc de Triomphe de l'Étoile:
Output{ "_id" : ObjectId("6105770952e6d1ebb707264a"), "name" : "The Eiffel Tower", "city" : "Paris", "country" : "France", "gps" : { "lat" : 48.858093, "lng" : 2.294694 }, "editor" : "Sammy" }
为了说明一次删除多个文档,请删除所有 Sammy
是编辑器的重要文档。 这将清空集合,因为您之前已将 Sammy
指定为每个纪念碑的编辑器:
db.monuments.deleteMany( { "editor": "Sammy" } )
这一次,MongoDB 让您知道此方法删除了六个文档:
Output{ "acknowledged" : true, "deletedCount" : 6 }
您可以通过计算其中的文档数来验证 monuments
集合现在是否为空:
db.monuments.count()
Output0
由于您刚刚从集合中删除了所有文档,因此此命令返回 0
的预期输出。
结论
通过阅读本文,您熟悉了 CRUD 操作的概念 — Create、Read、Update 和 Delete — 数据管理的四个基本组成部分。 您现在可以将新文档插入 MongoDB 数据库、修改现有文档、检索集合中已经存在的文档,还可以根据需要删除文档。
但请注意,本教程仅介绍了查询过滤的一种基本方法。 MongoDB 提供了一个强大的查询系统,允许根据复杂的标准精确地选择感兴趣的文档。 要了解有关创建更复杂查询的更多信息,我们建议您查看 有关主题 的官方 MongoDB 文档。