MongoDB有条件地按特定字段在数组中添加$ addToSet子文档

时间:2014-08-14 18:39:55

标签: mongodb mongodb-query

有没有办法根据数组子文档中的特定键字段有条件地$addToSet

这是我的意思的一个例子 - 给出由以下样本引导程序产生的集合;

cls
db.so.remove();
db.so.insert({
    "Name": "fruitBowl",
    "pfms" : [
        {
            "n" : "apples"
        }
    ]
});

n定义了一个唯一的文档密钥。我只希望一次在数组中具有相同n值的一个条目。所以我希望能够使用pfms更新n数组,以便我最终得到这个;

{
    "Name": "fruitBowl",
    "pfms" : [
        {
            "n" : "apples",
            "mState": 1111234
        }
    ]
}

这就是我现在所处的位置;

db.so.update({
  "Name": "fruitBowl",
},{
// not allowed to do this of course
//    "$pull": {
//  "pfms": { n: "apples" },
//    },

"$addToSet": {
  "pfms": {
    "$each": [
      {
        "n": "apples",
        "mState": 1111234
      }
    ]
   }
  }
 }
)

不幸的是,这会添加另一个数组元素;

db.so.find().toArray();

[
    {
        "Name" : "fruitBowl",
        "_id" : ObjectId("53ecfef5baca2b1079b0f97c"),
        "pfms" : [
            {
                "n" : "apples"
            },
            {
                "n" : "apples",
                "mState" : 1111234
            }
        ]
    }
]

我需要有效地将apples上匹配的n文档作为唯一标识符,并设置mState是否已存在条目。遗憾的是,我无法在同一份文档中执行$pull$addToSet(我尝试过)。

我真正需要的是字典语义,但现在不是一个选项,也不是打破文档 - 任何人都可以提出另一种方式吗?

FWIW - 现有格式是语言/驱动程序序列化的结果,我没有完全选择它。

进一步

在我知道已经存在的数组元素的情况下,我已经进一步了解我可以做到这一点;

db.so.update({
    "Name": "fruitBowl",
    "pfms.n": "apples",
},{
  $set: {
   "pfms.$.mState": 1111234,
  },
 }
)

但当然这只有效;

  1. 表示单个数组元素
  2. 只要我知道它存在
  3. 第一个限制不是灾难,但如果我无法有效地将$addToSet与之前的$set进行升级或合并(当然我无法做到),那么我现在能想到的唯一解决方法就是两次数据库往返。

5 个答案:

答案 0 :(得分:5)

$addToSet运算符当然要求“添加到集合中”的“整个”文档实际上是唯一的,因此您不能更改文档的“部分”或以其他方式将其视为“部分”匹配”。

您偶然发现使用$pull删除任何会导致“重复”的“密钥”字段的元素的最佳方法,但当然您无法在不同的更新运算符中修改相同的路径。

所以你最接近的事情是发布单独的操作,但也使用MongoDB 2.6引入的"Bulk Operations API"。这允许两者同时发送到服务器,以便最接近您将获得的“连续”操作列表:

var bulk = db.so.initializeOrderedBulkOp();

bulk.find({ "Name": "fruitBowl", "pfms.n": "apples": }).updateOne({ 
    "$pull": { "pfms": { "n": "apples" } }
});
bulk.find({ "Name": "fruitBowl" }).updateOne({
    "$push": { "pfms": { "n": "apples", "state": 1111234 } }
})

bulk.execute();

如果将元素移动到另一个集合并依赖于“upserts”和$set以便在集合而不是数组上具有相同的功能,那么这几乎是你最好的方法

答案 1 :(得分:3)

我遇到了完全相同的情况。我在帖子中插入和删除了喜欢的内容。

我所做的是,使用mongoose findOneAndUpdate函数(类似于mongodb中的update或findAndModify函数)。

关键概念是

    字段 时,
  • 插入
  • 字段 存在时
  • 删除

插入

findOneAndUpdate({ _id: theId, 'likes.userId': { $ne: theUserId }},
{ $push: { likes: { userId: theUserId, createdAt: new Date() }}},
{ 'new': true }, function(err, post) { // do the needful }); 

删除

findOneAndUpdate({ _id: theId, 'likes.userId': theUserId},
{ $pull: { likes: { userId: theUserId }}},
{ 'new': true }, function(err, post) { // do the needful }); 

这使整个操作成为原子,并且 userId 字段没有重复。

我希望这有助于此。如果您有任何疑问,请随时提出。

答案 2 :(得分:0)

作为商业分析师,我遇到了同样的问题,希望在经过数小时的调查后我能找到解决方案。

// The customer document: 

{
   "id" : "1212",
    "customerCodes" : [ 
        {
            "code" : "I"          
        },
        {
            "code" : "YK"          
        }        
    ]
}

//问题:我想将dateField“01.01.2016”插入客户文档,其中customerCodes子文档的文档包含代码“YK”但没有dateField。最终文件必须如下:

{
   "id" : "1212",
    "customerCodes" : [ 
        {
            "code" : "I"          
        },
        {
            "code" : "YK" , 
            "dateField" : "01.01.2016"
        }        
    ]
}

//解决方案:解决方案代码分为三个步骤:

//第1部分 - 使用customerCodes“YK”查找客户,但没有dateField

// PART 2 - 在customerCodes列表中找到带有“YK”的子文档的索引。

//第3部分 - 将值插入文档

//这是代码

    // PART 1 
    var myCursor = db.customers.find({ customerCodes:{$elemMatch:{code:"YK", dateField:{ $exists:false} }}});

    // PART 2 
    myCursor.forEach(function(customer){       
         if(customer.customerCodes != null ) 
         {           
            var size = customer.customerCodes.length;  
            if( size > 0 )  
            {
                var iFoundTheIndexOfSubDocument= -1; 
                var index = 0;     
                customer.customerCodes.forEach( function(clazz)
                {                           
                    if(  clazz.code == "YK" && clazz.changeDate == null ) 
                    {
                        iFoundTheIndexOfSubDocument = index;                 

                    }
                    index++;  
                }) 

                // PART 3
                // What happens here is : If i found the indice of the 
                // "YK" subdocument, I create "updates" document which   
//                 corresponds to the new data to be inserted`                       
                //
                if( iFoundTheIndexOfSubDocument != -1 ) 
                {
                    var toSet = "customerCodes."+ iFoundTheIndexOfSubDocument +".dateField";
                    var updates = {};
                    updates[toSet] = "01.01.2016";

                    db.customers.update({ "id" : customer.id } , { $set: updates }); 
    // This statement is actually interpreted like this : 
    // db.customers.update({ "id" : "1212" } ,{ $set: customerCodes.0.dateField : "01.01.2016" });
                }
            }                                       
         }
    }); 

祝你有愉快的一天!

答案 3 :(得分:0)

据我所知,MongoDB现在(来自v 4.2)允许使用aggregation pipelines for updates。 使它正常工作的优雅方式如下(根据问题):

db.runCommand({
  update: "your-collection-name",
  updates: [
    {
      q: {},
      u: {
        $set: {
          "pfms.$[elem]": {
            "n":"apples",
            "mState": NumberInt(1111234)
          }
        }
      },
      arrayFilters: [
        {
          "elem.n": {
            $eq: "apples"
          }
        }
      ],
      multi: true
    }
  ]
})

答案 4 :(得分:0)

在我的场景中,数据不存在时需要初始化,如果存在则更新字段,数据不会被删除。如果数据有这些状态,您可能想尝试以下方法。

// Mongoose, but mostly same as mongodb
    // Update the tag to user, If there existed one.
    const user = await UserModel.findOneAndUpdate(
      {
        user: userId,
        'tags.name': tag_name,
      },
      {
        $set: {
          'tags.$.description': tag_description,
        },
      }
    )
      .lean()
      .exec();

    // Add a default tag to user
    if (user == null) {
      await UserModel.findOneAndUpdate(
        {
          user: userId,
        },
        {
          $push: {
            tags: new Tag({
              name: tag_name,
              description: tag_description,
            }),
          },
        }
      );
    }

这是场景中最干净、最快速的方法。