这是一个棘手的问题,但这里有一些充分的理由,正常 MongoDB 文档案例不会包含这样的“点字段”。所以确实需要一些特殊的处理。
这里的基本情况是,您基本上需要将包含“点字段”的文档中的"key" 转换为实际上是“字符串”,然后只需在其中查找"component" 的存在字符串。
简短的情况是你基本上想要一个pipeline 表达式为你的watch() 像这样:
const pipeline = [
{ "$match": {
"$expr": {
"$ne": [
{ "$size": {
"$filter": {
"input": {
"$objectToArray": "$updateDescription.updatedFields"
},
"cond": {
"$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
}
}
}},
0
]
}
}}
];
这样做是使用$objectToArray 将updatedFields 对象转换为k 和v 属性的数组,而不是命名键。此时结果值将如下所示:
[
{
"k": "coponents.weapon",
"v": "Sword"
}
]
这允许 now 数组 与 $filter 操作一起使用,该操作使用来自 $indexOfCP 的表达式来测试 k 属性值中是否存在字符串。如果它是 -1 以外的任何内容(对于 not found ),那么在该值中包含 "component" 的任何元素都将是唯一保留的内容,并且匹配结果数组中的元素。
注意如果您有 MongoDB 4.2,您可能需要查看 $regexMatch 运算符来代替 $indexOfCP。不需要简单地测试字符串中字符串的“存在”,但是如果您的用例需要,正则表达式当然可以做更多的事情。
在这里合适,您可能会从字符串的开头搜索到包含的“点”,将cond 中的$filter 替换为:
"$not": { "$regexMatch": { "input": "$$this.k", "regex": /^components\./ } }
由于它是作为数组返回的,因此您可以测试 $size 以查看现在 已过滤 数组在删除 "component" 值后是否实际上还剩下任何东西.如果不是,并且大小确实为0,则通过$expr 丢弃结果,这也是允许在$match 中使用聚合表达式的主要运算符。
当然,所做的只是选择文档,这些文档实际上可以返回。如果您在 Change Stream 文档中指示的 update 结果中可能有其他更改的字段,那么您实际上需要对其使用相同类型的 $filter 操作,以便从结果:
{ "$addFields": {
"updateDescription": {
"updatedFields": {
"$arrayToObject": {
"$filter": {
"input": {
"$objectToArray": "$updateDescription.updatedFields"
},
"cond": {
"$eq": [{ "$indexOfCP": [ "$$this.k", "component" ] }, -1 ]
}
}
}
}
}
}
}
注意这里添加了$arrayToObject,它基本上反转所使用的操作过程,然后实际上将updatedFields内容返回为其原始形式,没有不需要的键。
为了演示,这里是一个实际的完整列表,它重现了对具有这种结构的集合所做的更改,并包含 watcher 管道以静音不需要的变化:
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017';
const options = { useNewUrlParser: true, useUnifiedTopology: true };
const log = doc => console.log(JSON.stringify(doc, undefined, 2));
(async function() {
try {
let client = await MongoClient.connect(uri, options);
let db = client.db('test');
// Insert some starting data
await db.collection('things').deleteMany();
await db.collection('things').insertOne({
components: {
weapon: { type: "Sword" },
health: { value: 10 }
},
type: "Monster"
});
// Set up the changeStream
const pipeline = [
// Filters documents
{ "$match": {
"$expr": {
"$ne": [
{ "$size": {
"$filter": {
"input": {
"$objectToArray": "$updateDescription.updatedFields"
},
"cond": {
"$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
/* Alternate MongoDB 4.2 syntax
"$not": {
"$regexMatch": {
"input": "$$this.k",
"regex": /^components\./
}
}
*/
}
}
}},
0
]
}
}},
/* -- Uncomment just to see the k and v structure
{ "$project": {
"update": { "$objectToArray": "$updateDescription.updatedFields" }
}}
*/
// Actually removes the keys and returns only non filtered
{ "$addFields": {
"updateDescription": {
"updatedFields": {
"$arrayToObject": {
"$filter": {
"input": {
"$objectToArray": "$updateDescription.updatedFields"
},
"cond": {
"$eq": [{ "$indexOfCP": [ "$$this.k", "component" ] }, -1 ]
/* Alternate MongoDB 4.2 syntax
"$not": {
"$regexMatch": {
"input": "$$this.k",
"regex": /^components\./
}
}
*/
}
}
}
}
}
}
}
];
const changeStream = db.collection('things').watch(pipeline);
changeStream.on('change', next => log({ changeDocument: next }));
// Loop some changes
await new Promise(async (resolve, reject) => {
let tick = true;
setInterval(async () => {
try {
let { value }= await db.collection('things')
.findOneAndUpdate(
{},
{ $set: { 'components.weapon': (tick) ? 'Knife' : 'Sword' }},
{ returnOriginalDocument: false }
);
tick = !tick; // flip the boolean
log({ currentDoc: value });
} catch (e) {
reject(e);
}
},2000)
});
} catch (e) {
console.error(e)
}
})()