【发布时间】:2015-11-08 05:32:22
【问题描述】:
根据文件:
$pull 运算符从现有数组中删除一个或多个与指定条件匹配的值的所有实例。
是否可以选择仅删除值的第一个实例?例如:
var array = ["bird","tiger","bird","horse"]
如何在更新调用中直接删除第一个“鸟”?
【问题讨论】:
标签: node.js mongodb mongodb-query
根据文件:
$pull 运算符从现有数组中删除一个或多个与指定条件匹配的值的所有实例。
是否可以选择仅删除值的第一个实例?例如:
var array = ["bird","tiger","bird","horse"]
如何在更新调用中直接删除第一个“鸟”?
【问题讨论】:
标签: node.js mongodb mongodb-query
所以您是正确的,$pull 运算符完全按照文档中的说明进行操作,因为它的参数实际上是用于匹配要删除的元素的“查询”。
如果您的数组内容碰巧总是在您显示的“第一个”位置有元素,那么$pop 运算符实际上会删除第一个元素。
使用基本节点驱动:
collection.findOneAndUpdate(
{ "array.0": "bird" }, // "array.0" is matching the value of the "first" element
{ "$pop": { "array": -1 } },
{ "returnOriginal": false },
function(err,doc) {
}
);
对于 mongoose,返回修改后文档的参数不同:
MyModel.findOneAndUpdate(
{ "array.0": "bird" },
{ "$pop": { "array": -1 } },
{ "new": true },
function(err,doc) {
}
);
但如果不知道要删除的“第一个”项的数组位置,则两者都没有多大用处。
对于这里的一般方法,您需要“两个”更新,一个是匹配第一个项目并将其替换为要删除的唯一内容,第二个是实际删除修改后的项目。
如果应用简单更新而不要求返回文档,这会简单得多,也可以跨文档批量完成。使用async.series 之类的内容也有助于避免嵌套调用:
async.series(
[
function(callback) {
collection.update(
{ "array": "bird" },
{ "$unset": { "array.$": "" } },
{ "multi": true }
callback
);
},
function(callback) {
collection.update(
{ "array": null },
{ "$pull": { "array": null } },
{ "multi": true }
callback
);
}
],
function(err) {
// comes here when finished or on error
}
);
因此在此处使用$unset 和positional $ 运算符可以将“第一个”项目更改为null。然后使用$pull 的后续查询只是从数组中删除任何null 条目。
这就是你如何安全地从数组中删除“第一次”出现的值。确定该数组是否包含多个相同的值是另一个问题。
【讨论】:
值得注意的是,虽然这里的另一个答案确实是正确的,但这里的一般方法是 $unset 匹配的数组元素以创建 null 值,然后 $pull 只是 null 值从数组中,有更好的方法在现代 MongoDB 版本中实现这一点。
bulkWrite()
作为提交 两个 操作以作为单独请求按顺序更新的替代情况,现代 MongoDB 版本通过 推荐 @ 支持 批量操作 987654323@ 方法允许将多个更新作为 single 请求提交,并带有 single 响应:
collection.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
与显示 两个 请求的答案相同,但这次只是 一个。这可以节省大量服务器通信的开销,因此通常是更好的方法。
随着 MongoDB 4.2 的发布,aggregation expressions 现在可以在 MongoDB 的各种“更新”操作中使用。这是$addFields、$set(这是$addFields 的别名,旨在使这些“更新”语句更符合逻辑地阅读)、$project 或$replaceRoot 的单个管道阶段,并且它是自己的别名@987654329 @。 $redact 流水线阶段在某种程度上也适用于此。基本上任何返回 "resized" 文档的管道阶段都是允许的。
collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
在这种情况下,操作是用来实现$slice和$indexOfArray操作符,基本上拼凑一个新数组“跳过” 在 first 匹配的数组元素上。这些片段通过$concatArrays 运算符加入,返回一个缺少第一个匹配元素的新数组。
现在这可能更有效,因为仍然是单个请求的操作现在也是单个操作,并且会减少一点服务器 em> 开销。
当然,唯一的问题是 MongoDB 4.2 之前的任何版本都不支持此功能。另一方面,bulkWrite() 可能是一个较新的 API 实现,但对服务器的实际底层调用将应用回 MongoDB 2.6 实现实际的“批量 API”调用,甚至通过所有核心驱动程序回归到早期版本实际实现这个方法。
作为演示,这里列出了这两种方法:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useCreateIndex', true);
mongoose.set('useFindAndModify', false);
const arrayTestSchema = new Schema({
array: [String]
});
const ArrayTest = mongoose.model('ArrayTest', arrayTestSchema);
const array = ["bird", "tiger", "horse", "bird", "horse"];
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
await ArrayTest.create({ array });
// Use bulkWrite update
await ArrayTest.bulkWrite(
[
{ "updateOne": {
"filter": { "array": "bird" },
"update": {
"$unset": { "array.$": "" }
}
}},
{ "updateOne": {
"filter": { "array": null },
"update": {
"$pull": { "array": null }
}
}}
]
);
log({ bulkWriteResult: (await ArrayTest.findOne()) });
// Use agggregation expression
await ArrayTest.collection.updateOne(
{ "array": "horse" },
[
{ "$set": {
"array": {
"$concatArrays": [
{ "$slice": [ "$array", 0, { "$indexOfArray": [ "$array", "horse" ] }] },
{ "$slice": [
"$array",
{ "$add": [{ "$indexOfArray": [ "$array", "horse" ] }, 1] },
{ "$size": "$array" }
]}
]
}
}}
]
);
log({ aggregateWriteResult: (await ArrayTest.findOne()) });
} catch (e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
还有输出:
Mongoose: arraytests.deleteMany({}, {})
Mongoose: arraytests.insertOne({ array: [ 'bird', 'tiger', 'horse', 'bird', 'horse' ], _id: ObjectId("5d8f509114b61a30519e81ab"), __v: 0 }, { session: null })
Mongoose: arraytests.bulkWrite([ { updateOne: { filter: { array: 'bird' }, update: { '$unset': { 'array.$': '' } } } }, { updateOne: { filter: { array: null }, update: { '$pull': { array: null } } } } ], {})
Mongoose: arraytests.findOne({}, { projection: {} })
{
"bulkWriteResult": {
"array": [
"tiger",
"horse",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
Mongoose: arraytests.updateOne({ array: 'horse' }, [ { '$set': { array: { '$concatArrays': [ { '$slice': [ '$array', 0, { '$indexOfArray': [ '$array', 'horse' ] } ] }, { '$slice': [ '$array', { '$add': [ { '$indexOfArray': [ '$array', 'horse' ] }, 1 ] }, { '$size': '$array' } ] } ] } } } ])
Mongoose: arraytests.findOne({}, { projection: {} })
{
"aggregateWriteResult": {
"array": [
"tiger",
"bird",
"horse"
],
"_id": "5d8f509114b61a30519e81ab",
"__v": 0
}
}
注意:示例清单使用mongoose,部分原因是在给出的其他答案中引用了它,部分原因还在于使用 aggregate 语法证明了一个重要点例子。请注意,代码使用
ArrayTest.collection.updateOne(),因为在当前版本的 Mongoose(撰写本文时为 5.7.1)中,此类更新的 聚合管道 语法已被标准删除猫鼬模型方法。因此,
.collection访问器可用于从核心 MongoDB 节点驱动程序获取底层Collection对象。在对允许包含此表达式的 mongoose 进行修复之前,这是必需的。
【讨论】: