查询 MongoDB — Node.js SDK
在此页面上
您可以通过使用Realm Node.js SDK 的 MongoDB客户端和Query API直接从客户端应用程序代码查询存储在MongoDB Atlas中的数据。 Atlas App Services提供集合的数据访问规则,以便根据登录用户或每个文档的内容安全地检索结果。
用例
您可能出于多种原因想要查询 MongoDB 数据源。通过 Atlas Device Sync 处理客户端中的数据并不总是可行或可能的。您可能希望在以下情况下查询 MongoDB:
数据集较大或客户端设备有限制,无法加载整个数据集
您正在创建或更新自定义用户数据
您正在检索未在 Realm 中建模的文档
您的应用程序需要访问没有严格模式的集合
非 Realm 服务会生成要访问的集合
以上查询场景虽未穷尽,却是直接查询 MongoDB 的一些常见使用案例。
先决条件
从 Node.js 应用程序查询 MongoDB 之前,必须在 App Services App 中设置 MongoDB 数据访问权限。要了解如何设置后端应用程序以让 Realm SDK 查询 Atlas,请参阅 App Services 文档中的设置 MongoDB 数据访问权限。
例子
示例数据
本页上的示例使用以下 MongoDB 集合,而该集合描述了连锁植物商店中待售的各种植物:
{ _id: ObjectId("5f87976b7b800b285345a8c4"), name: "venus flytrap", sunlight: "full", color: "white", type: "perennial", _partition: "Store 42" }, { _id: ObjectId("5f87976b7b800b285345a8c5"), name: "sweet basil", sunlight: "partial", color: "green", type: "annual", _partition: "Store 42" }, { _id: ObjectId("5f87976b7b800b285345a8c6"), name: "thai basil", sunlight: "partial", color: "green", type: "perennial", _partition: "Store 42" }, { _id: ObjectId("5f87976b7b800b285345a8c7"), name: "helianthus", sunlight: "full", color: "yellow", type: "annual", _partition: "Store 42" }, { _id: ObjectId("5f87976b7b800b285345a8c8"), name: "petunia", sunlight: "full", color: "purple", type: "annual", _partition: "Store 47" }
plants
集合中的文档使用以下模式:
{ "title": "Plant", "bsonType": "object", "required": ["_id", "_partition", "name"], "properties": { "_id": { "bsonType": "objectId" }, "_partition": { "bsonType": "string" }, "name": { "bsonType": "string" }, "sunlight": { "bsonType": "string" }, "color": { "bsonType": "string" }, "type": { "bsonType": "string" } } }
type Plant = { _id: BSON.ObjectId; _partition: string; name: string; sunlight?: string; color?: string; type?: string; };
连接到关联集群
要从客户端应用程序访问关联集群,请将集群名称传递给 User.mongoClient()。这将返回一个 MongoDB 服务接口,您可以使用该接口访问集群中的数据库和集合。
const mongodb = app.currentUser.mongoClient("mongodb-atlas"); const plants = mongodb.db("example").collection("plants");
const mongodb = app.currentUser.mongoClient("mongodb-atlas"); const plants = mongodb.db("example").collection<Plant>("plants");
读取操作
查找单个文档
要查找单个文档,请将与该文档匹配的查询传递给collection.findOne() 。 如果未传递查询, findOne()
则会匹配它在集合中找到的第一个文档。
以下代码段在描述一组商店中待售植物的文档集合中查找描述“venus flytrap”植物的文档:
const venusFlytrap = await plants.findOne({ name: "venus flytrap" }); console.log("venusFlytrap", venusFlytrap);
{ _id: ObjectId("5f87976b7b800b285345a8c4"), name: "venus flytrap", sunlight: "full", color: "white", type: "perennial", _partition: "Store 42", }
查找多个文档
要查找多个文档,请将与文档匹配的查询传递给collection.find() 。 如果未传递查询,则find()
会匹配集合中的所有文档。
以下代码片段在描述一组商店中待售植物的文档集合中查找所有描述多年生植物的文档:
const perennials = await plants.find({ type: "perennial" }); console.log("perennials", perennials);
[ { _id: ObjectId("5f87976b7b800b285345a8c4"), name: 'venus flytrap', sunlight: 'full', color: 'white', type: 'perennial', _partition: 'Store 42' }, { _id: ObjectId("5f87976b7b800b285345a8c6"), name: 'thai basil', sunlight: 'partial', color: 'green', type: 'perennial', _partition: 'Store 42' }, { _id: ObjectId("5f879f83fc9013565c23360e"), name: 'lily of the valley', sunlight: 'full', color: 'white', type: 'perennial', _partition: 'Store 47' }, { _id: ObjectId("5f87a0defc9013565c233611"), name: 'rhubarb', sunlight: 'full', color: 'red', type: 'perennial', _partition: 'Store 47' }, { _id: ObjectId("5f87a0dffc9013565c233612"), name: 'wisteria lilac', sunlight: 'partial', color: 'purple', type: 'perennial', _partition: 'Store 42' }, { _id: ObjectId("5f87a0dffc9013565c233613"), name: 'daffodil', sunlight: 'full', color: 'yellow', type: 'perennial', _partition: 'Store 42' } ]
计算文档
要对文档进行计数,请将与文档匹配的查询传递给collection.count() 。 如果未传递查询, count()
则会计算集合中所有文档的数量。
以下代码段计算描述一组商店中待售植物的文档集合中的文档数量:
const numPlants = await plants.count(); console.log(`There are ${numPlants} plants in the collection`);
"There are 9 plants in the collection"
写入操作
插入单一文档
要插入单个文档,请将其传递给collection.insertOne()。
以下代码片段会将描述“铃兰”植物的单个文档插入到描述一组商店中待售植物的文档集合中:
const result = await plants.insertOne({ name: "lily of the valley", sunlight: "full", color: "white", type: "perennial", _partition: "Store 47", }); console.log(result);
{ insertedId: "5f879f83fc9013565c23360e", }
插入多个文档
要同时插入多个文档,请将它们作为数组传递给 collection.insertMany()。
以下代码段将三个描述植物的文档插入到描述一组商店中待售植物的文档集合中:
const result = await plants.insertMany([ { name: "rhubarb", sunlight: "full", color: "red", type: "perennial", _partition: "Store 47", }, { name: "wisteria lilac", sunlight: "partial", color: "purple", type: "perennial", _partition: "Store 42", }, { name: "daffodil", sunlight: "full", color: "yellow", type: "perennial", _partition: "Store 42", }, ]); console.log(result);
{ insertedIds: [ "5f87a0defc9013565c233611", "5f87a0dffc9013565c233612", "5f87a0dffc9013565c233613", ], }
更新单份文档
要更新单个文档,请将与该文档匹配的查询和更新文档传递给collection.updateOne()。
以下代码段更新文档集合中的单个文档,这些文档描述了一组商店中待售的植物。 此操作查询name
字段包含值“petunia”的文档,并将第一个匹配文档的sunlight
字段的值更改为“partial”:
const result = await plants.updateOne( { name: "petunia" }, { $set: { sunlight: "partial" } } ); console.log(result);
{ matchedCount: 1, modifiedCount: 1 }
更新多个文档
要同时更新多个文档,请将与这些文档匹配的查询以及更新描述传递给 collection.updateMany()。
以下代码段更新文档集合中的多个文档,这些文档描述一组商店中待售的植物。 此操作查询_partition
字段包含值“Store 47 ”的文档,并将每个匹配文档的_partition
字段的值更改为“Store 51 ”:
const result = await plants.updateMany( { _partition: "Store 47" }, { $set: { _partition: "Store 51" } } ); console.log(result);
{ matchedCount: 3, modifiedCount: 3 }
更新或插入文档
要插入文档,请在更新操作中将 upsert
选项设为 true
。如果此操作的查询与集合中的所有文档均不匹配,upsert 操作则会自动将单个新文档插入到集合中,而该新文档与所提供的查询文档及其应用的更新相匹配。
以下代码片段将通过一个 upsert 操作来更新文档集合中的某一文档,而这些文档描述了一组商店中出售的植物。此查询与所有现有文档均不匹配,因此 MongoDB 会自动创建一个新文档。
const result = await plants.updateOne( { sunlight: "full", type: "perennial", color: "green", _partition: "Store 47", }, { $set: { name: "sweet basil" } }, { upsert: true } ); console.log(result);
{ matchedCount: 0, modifiedCount: 0, upsertedId: ObjectId("5f1f63055512f2cb67f460a3"), }
删除单个文档
要从集合中删除单个文档,请将与此文档匹配的查询传递给 collection.deleteOne()。如果未传递查询,或是此查询匹配多个文档,此操作则会删除其找到的第一个文档。
以下代码段删除描述一组商店中待售植物的文档集合中的一个文档。 此操作查询color
字段值为“green”的文档,并删除与查询匹配的第一个文档:
const result = await plants.deleteOne({ color: "green" }); console.log(result);
{ deletedCount: 1 }
删除多个文档
要从集合中删除多个文档,请将与文档匹配的查询传递给collection.deleteMany() 。 如果未传递查询, deleteMany()
将删除集合中的所有文档。
以下代码片段删除“Store 51 ”中植物的所有文档,而这些文档位于描述一组商店中待售植物的文档集合中:
const result = await plants.deleteMany({ _partition: "Store 51", }); console.log(result);
{ deletedCount: 3 }
实时变更通知
您可以调用 collection.watch() 来订阅 MongoDB 在添加、修改或删除集合中的文档时发出的实时变更通知。每条通知均会指定已更改的文档、更改方式以及引发此事件的操作完成后的完整文档。
重要
无服务器限制
如果数据源为 Atlas 无服务器实例,则无法监控是否出现更改。MongoDB 无服务器当前不支持变更流,而变更流可用于受监视的集合以侦听是否存在更改。
监视集合中的所有更改
要监视集合中的所有更改,请调用 collection.watch() 且不带任何参数:
for await (const change of plants.watch()) { switch (change.operationType) { case "insert": { const { documentKey, fullDocument } = change; console.log(`new document: ${documentKey}`, fullDocument); break; } case "update": { const { documentKey, fullDocument } = change; console.log(`updated document: ${documentKey}`, fullDocument); break; } case "replace": { const { documentKey, fullDocument } = change; console.log(`replaced document: ${documentKey}`, fullDocument); break; } case "delete": { const { documentKey } = change; console.log(`deleted document: ${documentKey}`); break; } } }
监视集合中的特定更改
要监视集合中的特定变化,请将匹配变更事件字段的查询传递给 collection.watch():
for await (const change of plants.watch({ filter: { operationType: "insert", "fullDocument.type": "perennial", }, })) { // The change event will always represent a newly inserted perennial const { documentKey, fullDocument } = change; console.log(`new document: ${documentKey}`, fullDocument); }
聚合操作
聚合操作会通过一系列名为聚合管道的阶段来运行集合中的所有文档。聚合允许您过滤和转换文档、收集有关相关文档群组的摘要数据以及其他复杂的数据操作。
运行聚合管道
要执行聚合管道,请将聚合阶段数组传递给 collection.aggregation()。聚合操作返回管道中最后一个阶段的结果集。
以下代码片段将按 type
值对 plants
集合中的所有文档进行分组,并汇总每种类型的数量:
const result = await plants.aggregate([ { $group: { _id: "$type", total: { $sum: 1 }, }, }, { $sort: { _id: 1 } }, ]); console.log(result);
[ { _id: "annual", total: 1 }, { _id: "perennial", total: 5 }, ]
筛选文档
您可以使用$match阶段根据标准 MongoDB查询语法筛选文档。
{ "$match": { "<Field Name>": <Query Expression>, ... } }
例子
以下 $match
阶段会筛选文档,以仅包含 type
字段值等于“perennial”的文档:
const perennials = await plants.aggregate([ { $match: { type: { $eq: "perennial" } } }, ]); console.log(perennials);
[ { "_id": ObjectId("5f87976b7b800b285345a8c4"), "_partition": "Store 42", "color": "white", "name": "venus flytrap", "sunlight": "full", "type": "perennial" }, { "_id": ObjectId("5f87976b7b800b285345a8c6"), "_partition": "Store 42", "color": "green", "name": "thai basil", "sunlight": "partial", "type": "perennial" }, { "_id": ObjectId("5f87a0dffc9013565c233612"), "_partition": "Store 42", "color": "purple", "name": "wisteria lilac", "sunlight": "partial", "type": "perennial" }, { "_id": ObjectId("5f87a0dffc9013565c233613"), "_partition": "Store 42", "color": "yellow", "name": "daffodil", "sunlight": "full", "type": "perennial" }, { "_id": ObjectId("5f1f63055512f2cb67f460a3"), "_partition": "Store 47", "color": "green", "name": "sweet basil", "sunlight": "full", "type": "perennial" } ]
对文档分组
您可以使用$group阶段聚合一个或多个文档的摘要数据。 MongoDB 根据$group
阶段的_id
字段中定义的表达式对文档进行分组。 您可以通过在字段名称前加上$
前缀来引用特定的文档字段。
{ "$group": { "_id": <Group By Expression>, "<Field Name>": <Aggregation Expression>, ... } }
例子
以下 $group
阶段将按文档的 type
字段值对文档进行排列,并计算每个唯一 type
值出现的植物文档的数量。
const result = await plants.aggregate([ { $group: { _id: "$type", numItems: { $sum: 1 }, }, }, { $sort: { _id: 1 } }, ]); console.log(result);
[ { _id: "annual", numItems: 1 }, { _id: "perennial", numItems: 5 }, ]
对文档进行分页
要对结果进行分页,您可以将范围聚合查询与$match
、 $sort
和$limit
操作符结合使用。 要了解有关对文档进行分页的更多信息,请参阅 MongoDB Server 文档中的使用范围查询。
例子
以下示例将按升序对文档集合进行分页。
// Paginates through list of plants // in ascending order by plant name (A -> Z) async function paginateCollectionAscending( collection, nPerPage, startValue ) { const pipeline = [{ $sort: { name: 1 } }, { $limit: nPerPage }]; // If not starting from the beginning of the collection, // only match documents greater than the previous greatest value. if (startValue !== undefined) { pipeline.unshift({ $match: { name: { $gt: startValue }, }, }); } const results = await collection.aggregate(pipeline); return results; } // Number of results to show on each page const resultsPerPage = 3; const pageOneResults = await paginateCollectionAscending( plants, resultsPerPage ); const pageTwoStartValue = pageOneResults[pageOneResults.length - 1].name; const pageTwoResults = await paginateCollectionAscending( plants, resultsPerPage, pageTwoStartValue ); // ... can keep paginating for as many plants as there are in the collection
项目文档字段
您可以使用 $project 阶段包含或省略文档中的特定字段,或使用聚合运算符计算新字段。投影有两种工作方式:
显式包含值为 1 的字段。此举的不利影响是会隐式排除所有未指定的字段。
隐式排除值为 0 的字段。此举的不利影响是会隐式包含所有未指定的字段。
这两种投影方法是互斥的:如果显式包含字段,则不能显式排除字段,反之亦然。
注意
_id
字段是一个特例:除非另行显式指定,否则它始终包含在每个查询中。因此,您可以排除具有 0
值的 _id
字段,同时包含其他字段,例如具有 1
的 _partition
。只有排除 _id
字段的特殊情况才允许在一个 $project
阶段中同时进行排除和包含。
{ "$project": { "<Field Name>": <0 | 1 | Expression>, ... } }
例子
以下 $project
阶段会省略 _id
字段,包含 name
字段,并创建一个名为 storeNumber
的新字段。storeNumber
是使用两个聚合运算符生成的:
$split
会将_partition
值分成位于空格字符两端的两个字符串段。例如,以此方式分割值“Store 42”会返回一个包含两个元素的数组:“Store”和“42”。$arrayElemAt
会根据第二个参数从数组中选择特定元素。在本例中,1
值会从$split
运算符生成的数组中选择第二个元素,因为数组索引会从0
开始。例如,传递给此操作的值 ["Store", "42"] 将返回值“42”。
const result = await plants.aggregate([ { $project: { _id: 0, name: 1, storeNumber: { $arrayElemAt: [{ $split: ["$_partition", " "] }, 1], }, }, }, ]); console.log(result);
[ { "name": "venus flytrap", "storeNumber": "42" }, { "name": "thai basil", "storeNumber": "42" }, { "name": "helianthus", "storeNumber": "42" }, { "name": "wisteria lilac", "storeNumber": "42" }, { "name": "daffodil", "storeNumber": "42" }, { "name": "sweet basil", "storeNumber": "47" } ]
向文档添加字段
您可以在$addFields阶段通过聚合操作符添加具有计算值的新字段。
{ $addFields: { <newField>: <expression>, ... } }
注意
$addFields
与 $project 类似,但不允许包含或省略字段。
例子
以下 $addFields
阶段将创建一个名为 storeNumber
的新字段,其中该字段的值为用于转换 _partition
字段值的两个聚合运算符的输出。
const result = await plants.aggregate([ { $addFields: { storeNumber: { $arrayElemAt: [{ $split: ["$_partition", " "] }, 1], }, }, }, ]); console.log(result);
[ { "_id": ObjectId("5f87976b7b800b285345a8c4"), "_partition": "Store 42", "color": "white", "name": "venus flytrap", "storeNumber": "42", "sunlight": "full", "type": "perennial" }, { "_id": ObjectId("5f87976b7b800b285345a8c6"), "_partition": "Store 42", "color": "green", "name": "thai basil", "storeNumber": "42", "sunlight": "partial", "type": "perennial" }, { "_id": ObjectId("5f87976b7b800b285345a8c7"), "_partition": "Store 42", "color": "yellow", "name": "helianthus", "storeNumber": "42", "sunlight": "full", "type": "annual" }, { "_id": ObjectId("5f87a0dffc9013565c233612"), "_partition": "Store 42", "color": "purple", "name": "wisteria lilac", "storeNumber": "42", "sunlight": "partial", "type": "perennial" }, { "_id": ObjectId("5f87a0dffc9013565c233613"), "_partition": "Store 42", "color": "yellow", "name": "daffodil", "storeNumber": "42", "sunlight": "full", "type": "perennial" }, { "_id": ObjectId("5f1f63055512f2cb67f460a3"), "_partition": "Store 47", "color": "green", "name": "sweet basil", "storeNumber": "47", "sunlight": "full", "type": "perennial" } ]
Unwind Array Values
您可以使用 $unwind 阶段将包含数组的单个文档转换为包含该数组中各个值的多个文档。当您展开数组字段时,MongoDB 会为该数组字段的每个元素复制每个文档一次,但同时还会在每个副本中用数组元素替换数组值。
{ $unwind: { path: <Array Field Path>, includeArrayIndex: <string>, preserveNullAndEmptyArrays: <boolean> } }
例子
以下示例将为每个对象的 type
和 color
组合使用 $unwind
阶段。该聚合管道包含以下步骤:
将
$group
阶段与$addToSet
结合使用,从而为每个type
创建新文档,同时附带新字段colors
,而该字段包含集合中出现的该花卉类型的所有颜色的数组。使用
$unwind
阶段可为每个类型与颜色的组合创建单独的文档。使用
$sort
阶段可按字母顺序对结果进行排序。
const result = await plants.aggregate([ { $group: { _id: "$type", colors: { $addToSet: "$color" } } }, { $unwind: { path: "$colors" } }, { $sort: { _id: 1, colors: 1 } }, ]); console.log(result);
[ { "_id": "annual", "colors": "yellow" }, { "_id": "perennial", "colors": "green" }, { "_id": "perennial", "colors": "purple" }, { "_id": "perennial", "colors": "white" }, { "_id": "perennial", "colors": "yellow" }, ]