MongoDB: $pull / $unset 多条件使用方法

3

示例文档:

{
  _id: 42,
  foo: {
    bar: [1, 2, 3, 3, 4, 5, 5]
  }
}

查询:

我想要“从foo.bar中删除所有$lt: 4的条目和第一个匹配$eq: 5的条目”。重要提示:$eq部分必须仅删除单个条目!

我有一个可行的解决方案,使用了3个更新查询,但对于这个简单的任务来说太多了。尽管如此,以下是我迄今为止所做的:

1. 查找第一个匹配$eq: 5的条目,并将其$unset。(正如您所知:$unset不会将其删除,而只是将其设置为null):

update(
  { 'foo.bar': 5 },
  { $unset: { 'foo.bar.$': 1 } } 
)

2. 使用$pull删除所有值为null的条目,以确保先前的5确实被删除:

update(
  {},
  { $pull: { 'foo.bar': null } } 
)

3. 使用$pull命令删除所有小于4的记录:

update(
  {},
  { $pull: { 'foo.bar': { $lt: 4 } } } 
)

生成的文档:

{
  _id: 42,
  foo: {
    bar: [4, 5]
  }
}

思路和想法:

  • 扩展查询1.,使其$unset条目$lt: 4和一个条目$eq: 5。然后我们可以执行查询2.,没有必要进行查询3.

  • 扩展查询2.,将匹配$or: [{$lt: 4}, {$eq: 5}]的所有内容都$pull掉。然后就没有必要进行查询3.

  • 扩展查询2.,将所有不匹配$not: { $gte: 4 }的内容都$pull掉。这个表达式应该匹配$lt: 4 $eq: null

我已经尝试实现这些查询,但有时它会抱怨查询语法,有时查询确实执行了,但什么也没删除。

如果有人有解决此问题的有效方法,那就太好了。

2个回答

3

不确定我是否完全理解您的意思,但是要“批量”更新文档,除了原始的$pull之外,您还可以采用以下方法,并添加一些“检测”,以确定需要从中删除“重复”5的文档:

// Remove less than four first
db.collection.update({},{ "$pull": { "foo.bar": { "$lt": 4 } } },{ "multi": true });

// Initialize Bulk
var bulk = db.collection.initializeOrderdBulkOp(),
    count = 0;

// Detect and cycle documents with duplicate five to be removed
db.collection.aggregate([
    // Project a "reduced" array and calculate if the same size as orig
    { "$project": { 
         "foo.bar": { "$setUnion": [ "$foo.bar", [] ] },
         "same": { "$eq": [
             { "$size": {  "$setUnion": [ "$foo.bar", [] ] } },
             { "$size": "$foo.bar" }
         ] }
    }},
    // Filter the results that were unchanged
    { "$match": { "same": true } }
]).forEach(function(doc) {
    bulk.find({ "_id": doc._id })
        .updateOne({ "$set": { "foo.bar": doc.foo.bar.sort() } });
    count++;

    // Execute per 1000 processed and re-init
    if ( count % 1000 == 0 ) {
        bulk.execute();
        bulk = db.collection.initializeOrderdBulkOp();
    }
});

// Clean up any batched
if ( count % 1000 != 0 )
    bulk.execute();

这段代码的作用是删除小于数字 "4" 的所有值以及所有重复项。其中,如果“集合”的长度不同,则认为它们是“重复项”。

如果您只想删除值为 5 的重复项,您可以采用类似的逻辑方法进行检测和修改,但不使用会将任何“重复项”删除的“集合运算符”,以便使其成为有效的“集合”。

无论如何,某种检测策略肯定比迭代更新好,直到“除一个之外”的值都被删除。


当然,您可以简化语句并删除一个更新操作。这不太美观,因为$pull不允许在查询中使用$or条件,但如果适用,我希望您能理解。

db.collection.update(
    { "foo.bar": 5 },
    { "$unset": { "foo.bar.$": 1 } },
    { "multi": true }
); // same approach

// So include all the values "less than four"
db.collection.update(
    { "foo.bar": { "$in": [1,2,3,null] } },
    { "$pull": { "foo.bar": { "$in": [1,2,3,null] } }},
    { "multi": true }
);

这个处理过程稍微简单一些,但是需要确保这些值都是整数。否则,请坚持使用您正在执行的三个更新步骤,这比在代码中循环要好。

供参考的“更好”的语法,但不幸的是无法使用,应该类似于以下内容:

db.collection.update(
    { 
        "$or": [
            { "foo.bar": { "$lt": 4 } },
            { "foo.bar": null }
        ]
    },
    { 
        "$pull": { 
            "$or": [
                { "foo.bar": { "$lt": 4 } },
                { "foo.bar": null }
            ]
        }
    },
    { "multi": true }
);

可能值得创建一个JIRA问题,但我认为主要是因为数组元素不是直接跟在$pull后面的“第一个”参数。


啊,抱歉。这不是关于去重的问题。实际上只是“删除所有小于4的内容和一个(或零个)等于5的条目”。 - Benjamin M
如果条件是 $lt: 4$eq: 5,那么以下是一些例子:[] -> [][2] -> [][4] -> [4][5] -> [][1,1,2,2,3,4,4] -> [4,4][1,4,5,5,5] -> [4,5,5][3,5] -> []。换句话说:“每个小于4的条目和一个等于5的条目(如果有任何等于5的条目)”。 - Benjamin M
很遗憾,您使用 $in 的查询无法工作,因为在我的真实场景中,这些数字是 ISODates,我需要使用 $lt 才能使其工作 :( - Benjamin M
@BenjaminM 嗯,那些点已经解释得很清楚了,说明了什么可以用,什么不能用。正如所提到的,在无法通过$in进行精确匹配(因为$in不接受$lt选项)的情况下,您最好采用目前正在使用的方法。您也不能执行`{“$pull”:{“foo.bar”:{“$lt”:4,“$gte”:null}}},因为那是一个and条件,不会匹配任何内容。一些人可能会发现这里的分析和结论有用。 - Blakes Seven
@BenjaminM 嗯,可能会被关闭,因为昨天我已经提交了这个问题。感谢你的提交,SERVER-1972。请在那里给该问题点赞。 - Blakes Seven
显示剩余3条评论

1
你可以使用Array.prototype.filter()Array.prototype.splice()方法。 filter()方法创建一个新数组,其中包含foo.bar$lt: 4,然后使用splice方法从foo.bar中删除这些值和第一个等于5的值。
var idx = [];
db.collection.find().forEach(function(doc){ 
    idx = doc.foo.bar.filter(function(el){  
        return el < 4;
    }); 
    for(var i in idx){   
        doc.foo.bar.splice(doc.foo.bar.indexOf(idx[i]), 1); 
    } 
    doc.foo.bar.splice(doc.foo.bar.indexOf(5), 1); 
    db.collection.save(doc);
} )

但这并不是安全的操作,如果在此期间对该数组进行了一些插入。 - Benjamin M
所以就记录一下,我认为@BenjaminM会同意这是关于“服务器”处理的。你总是可以在客户端代码中做这些事情,但这并不是真正高效的方法。更新不应该需要循环和“测试”集合中的每个文档才能生效。而且同意“这是不安全的”。.save()可能会“覆盖”。 - Blakes Seven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接