如何在MongoDB中执行CRUD操作

149 阅读16分钟

作者选择开放互联网/自由言论基金作为Write for DOnations计划的一部分,接受捐赠。

简介

MongoDB是一个持久的面向文档的数据库,用于存储和处理文档形式的数据。与其他数据库管理系统一样,MongoDB允许你通过四种基本类型的数据操作来管理和交互数据。

  • 创建操作,这涉及到向数据库写入数据
  • 读取操作,即查询数据库以获取其中的数据
  • 更新操作,改变数据库中已经存在的数据
  • 删除操作,从数据库中永久地删除数据

这四种操作被共同称为_CRUD_操作。

本教程概述了如何创建新的MongoDB文档,随后检索它们以读取其数据。它还解释了如何更新文档中的数据,以及如何在不再需要时删除文档。

前提条件

要学习本教程,你将需要。

  • 一台有普通的、非root用户的、具有sudo 权限的服务器,以及一个配置了UFW的防火墙。本教程使用运行Ubuntu 20.04的服务器进行验证,你可以按照Ubuntu 20.04的初始服务器设置教程来准备你的服务器。
  • 在你的服务器上安装MongoDB。要设置这个,请遵循我们的教程:如何在Ubuntu 20.04上安装MongoDB
  • 你的服务器的MongoDB实例通过启用认证和创建一个管理用户来保证安全。要像这样保护MongoDB,请遵循我们的教程:如何在Ubuntu 20.04上保护MongoDB
  • 基本熟悉本教程中使用的MongoDB外壳。要学习如何使用MongoDB外壳,请遵循《如何使用MongoDB外壳》教程。

**注意:**关于如何配置服务器、安装、然后确保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安全教程,你可以作为你在该指南第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来建立和管理一个世界各地著名历史纪念碑的目录。这个目录将存储每个纪念碑的名称、国家、城市和地理位置等信息。

这个目录中的文件将遵循类似于这个例子的格式,它代表吉萨金字塔

吉萨金字塔

{
    "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位置。这个位置是由一对纬度和经度坐标组成的,分别由latlng 字段表示,每个字段都持有浮点值。

**注意:**你可以在我们的概念性文章《面向文档的数据库介绍》中了解更多关于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 }}
])

注意这六个文件周围的方括号([] )。这些方括号标志着一个文件_阵列_。在方括号内,多个对象可以一个接一个地出现,用逗号分隔。在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() 方法读取你刚刚插入的数据。

第3步 - 读取文档

现在你的集合中存储了一些文档,你可以查询你的数据库来检索这些文档并读取它们的数据。这一步首先概述了如何查询一个给定集合中的所有文档,然后描述了如何使用过滤器来缩小检索到的文档列表。

在完成上一步后,你现在有七个描述著名古迹的文档被插入到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自动为**"国王之谷**"分配了一个对象标识符,其值为 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
  }
}

查询过滤器文件是相当强大和灵活的,它们允许你对集合文件应用复杂的过滤器。

第四步 - 更新文件

在MongoDB这样的面向文档的数据库中,文档随时间变化是很常见的。有时,它们的结构必须随着应用程序的需求变化而变化,或者数据本身可能会改变。这一步的重点是如何通过改变单个文档中的字段值来更新现有的文档,以及为一个集合中的每个文档添加一个新的字段。

insertOne()insertMany() 方法类似,MongoDB提供了一些方法,允许你一次性更新单个文档或多个文档。这些更新方法的一个重要区别是,在创建新的文档时,你只需要将文档数据作为方法参数传递。要更新集合中的一个现有文档,你还必须传递一个参数,指定你要更新的文档。

为了让用户做到这一点,MongoDB在更新方法中使用了与你在上一步中用来查找和检索文档的查询过滤器文档机制。任何可以用来检索文档的查询过滤器文档也可以用来指定要更新的文档。

试着把凯旋门的名字改为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() 将只更新多个结果中返回的_第一个_文档。

为了检查更新是否成功,尝试检索所有与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 }

这个输出告知你有7个文档被匹配,还有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() 方法,它只删除查询过滤器文档匹配的_第一个_文档,还有deleteMany() ,它一次删除多个对象。

要练习使用这些方法,首先要尝试删除你之前修改的凯旋门纪念碑。

db.monuments.deleteOne(
    { "name": "Arc de Triomphe de l'Étoile" }
)

注意,这个方法包括一个查询过滤器文件,就像之前的更新和检索例子一样。和以前一样,你可以使用任何有效的查询来指定哪些文件将被删除。

MongoDB将返回以下结果。

Output{ "acknowledged" : true, "deletedCount" : 1 }

这里,结果告诉你在这个过程中删除了多少个文档。

通过查询法国的纪念碑,检查文件是否真的从集合中被删除。

db.monuments.find({"country": "France"}).pretty()

这次该方法只返回单个纪念碑,即埃菲尔铁塔,因为你删除了凯旋门

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操作的概念--创建、读取、更新和删除--数据管理的四个基本组成部分。现在你可以在MongoDB数据库中插入新的文档,修改现有的文档,检索已经存在于集合中的文档,还可以根据需要删除文档。

不过要注意的是,本教程只涵盖了查询过滤的一种基本方式。MongoDB提供了一个强大的查询系统,允许根据复杂的标准精确选择感兴趣的文档。要了解更多关于创建更复杂的查询,我们鼓励你查看MongoDB的官方文档