【问题标题】:MongoDB: upsert sub-documentMongoDB:插入子文档
【发布时间】:2014-06-21 15:32:44
【问题描述】:

我有类似这样的文档,在 bars.name 上有一个唯一索引:

{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }

。我想更新{ name: 'foo', 'bars.name': 'qux' }$set: { 'bars.$.somefield': 2 } 所在的子文档,或者在{ name: 'foo' } 下创建一个带有{ name: 'qux', somefield: 2 } 的新子文档。

是否可以使用带有 upsert 的单个查询来执行此操作,还是我必须发出两个单独的查询?

相关:'upsert' in an embedded document(建议更改架构以将子文档标识符作为键,但这是从两年前开始的,我想知道现在是否有更好的解决方案。)

【问题讨论】:

    标签: mongodb mongodb-query upsert


    【解决方案1】:

    不,没有更好的解决方案,所以也许有一个解释。

    假设您有一个文件,其结构如您所见:

    { 
      "name": "foo", 
      "bars": [{ 
           "name": "qux", 
           "somefield": 1 
      }] 
    }
    

    如果你进行这样的更新

    db.foo.update(
        { "name": "foo", "bars.name": "qux" },
        { "$set": { "bars.$.somefield": 2 } },
        { "upsert": true }
    )
    

    然后一切都很好,因为找到了匹配的文档。但是如果你改变“bars.name”的值:

    db.foo.update(
        { "name": "foo", "bars.name": "xyz" },
        { "$set": { "bars.$.somefield": 2 } },
        { "upsert": true }
    )
    

    那么你就会失败。这里唯一真正改变的是,在 MongoDB 2.6 及更高版本中,错误更加简洁:

    WriteResult({
        "nMatched" : 0,
        "nUpserted" : 0,
        "nModified" : 0,
        "writeError" : {
            "code" : 16836,
            "errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
        }
    })
    

    这在某些方面更好,但你真的不想“更新”。您要做的是将元素添加到当前不存在“名称”的数组中。

    所以你真正想要的是没有“upsert”标志的更新尝试的“结果”,以查看是否有任何文档受到影响:

    db.foo.update(
        { "name": "foo", "bars.name": "xyz" },
        { "$set": { "bars.$.somefield": 2 } }
    )
    

    做出回应:

    WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
    

    所以当修改后的文档是0 时,你就知道你要发布以下更新:

    db.foo.update(
        { "name": "foo" },
        { "$push": { "bars": {
            "name": "xyz",
            "somefield": 2
        }}
    )
    

    确实没有其他方法可以完全按照您的意愿行事。由于对数组的添加并不是严格意义上的“设置”操作,因此您不能将$addToSet"bulk update" 功能结合使用,以便您可以“级联”您的更新请求。

    在这种情况下,您似乎需要检查结果,或者接受阅读整个文档并检查是否在代码中更新或插入新数组元素。

    【讨论】:

    • 是的,这就是我目前所拥有的——$set,如果没有找到匹配的文件,后跟 $push……只是想知道是否有更好的方法来解决这个问题。谢谢你的解释!
    • 如果您有多个进程试图同时更新,您如何使这个原子化。似乎可能存在竞争条件,您可能会多次调用 $push 并最终在数组中获得多个记录。有没有办法以原子方式插入新值?
    • @MichaelMoser 刚刚看到评论。但是通过添加像{ "name": "foo", "bars.name": { "$ne": "xyz" } } 这样的不等式测试作为查询,你可以确保你没有重复“推送”元素。
    • 第一个参数必须包含两个元素吗?我不能这样做:db.foo.update( { "name": "foo" }, { "$set": { "bars.$.somefield": 2 } }, { "upsert": true } ) 如果名称是唯一的吗?
    【解决方案2】:

    如果您不介意稍微更改架构并拥有这样的结构:

    { "name": "foo", "bars": { "qux": { "somefield": 1 },
                               "xyz": { "somefield": 2 },
                      }
    }
    

    您可以一次性执行您的操作。 重申 'upsert' in an embedded document 以确保完整性

    【讨论】:

    • 如果bars.name 的值无法提前知道,则此架构将变得难以验证和查询。
    【解决方案3】:

    有一种方法可以在两个查询中完成 - 但它仍然可以在 bulkWrite 中工作。

    这是相关的,因为在我的情况下,无法批处理是最大的问题。使用此解决方案,您无需收集第一个查询的结果,这样您就可以在需要时进行批量操作。

    以下是为您的示例运行的两个连续查询:

    // Update subdocument if existing
    collection.updateMany({
        name: 'foo', 'bars.name': 'qux' 
    }, {
        $set: { 
            'bars.$.somefield': 2 
        }
    })
    // Insert subdocument otherwise
    collection.updateMany({
        name: 'foo', $not: {'bars.name': 'qux' }
    }, {
        $push: { 
            bars: {
                somefield: 2, name: 'qux'
            }
        }
    })
    

    如果多个应用程序同时写入数据库,这还有一个额外的好处,即不会出现损坏的数据/竞争条件。如果两个应用程序同时运行相同的查询,您将不会冒险在文档中出现两个 bars: {somefield: 2, name: 'qux'} 子文档。

    【讨论】:

      【解决方案4】:

      我正在挖掘相同的功能,发现在 4.2 或更高版本中,MongoDB 提供了一个名为 Update with aggregation pipeline 的新功能。
      如果将此功能与其他一些技术一起使用,则可以通过单个查询实现 upsert subdocument 操作。

      这是一个非常冗长的查询,但我相信如果您知道 subCollection 上不会有太多记录,那么它是可行的。以下是如何实现此目的的示例:

      const documentQuery = { _id: '123' }
      const subDocumentToUpsert = { name: 'xyz', id: '1' }
      
      collection.update(documentQuery, [
          {
              $set: {
                  sub_documents: {
                      $cond: {
                          if: { $not: ['$sub_documents'] },
                          then: [subDocumentToUpsert],
                          else: {
                              $cond: {
                                  if: { $in: [subDocumentToUpsert.id, '$sub_documents.id'] },
                                  then: {
                                      $map: {
                                          input: '$sub_documents',
                                          as: 'sub_document',
                                          in: {
                                              $cond: {
                                                  if: { $eq: ['$$sub_document.id', subDocumentToUpsert.id] },
                                                  then: subDocumentToUpsert,
                                                  else: '$$sub_document',
                                              },
                                          },
                                      },
                                  },
                                  else: { $concatArrays: ['$sub_documents', [subDocumentToUpsert]] },
                              },
                          },
                      },
                  },
              },
          },
      ])
      

      【讨论】:

        猜你喜欢
        • 2016-04-14
        • 2013-11-11
        • 1970-01-01
        • 2013-02-13
        • 1970-01-01
        • 1970-01-01
        • 2023-04-05
        • 1970-01-01
        • 2012-08-29
        相关资源
        最近更新 更多