猫鼬更新数组或添加到数组

时间:2020-04-15 16:40:14

标签: node.js mongodb mongoose mongoose-schema

我已经尝试使它运行一段时间了,但是我不知道自己在做什么错。

我有两个这样的模式

const paymentSchema = new Schema({
    year_month: {
        type: String,
        required: true
    },
    status: {
        type: Boolean,
        required: true
    }
});

const testSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    payments: [{
        type: paymentSchema,
        required: false,
    }]
});

然后我要更新现有值,或者如果该值不可用,我想将其添加到数组中。

假设我在数据库中有以下值:

[
    {
        "_id": "5e90ae0e0ed9974174e92826",
        "name": "User 1",
        "payments": [
            {
                "_id": "5e90c3fb79bba9571ae58a66",
                "year_month": "2020_02",
                "status": false
            }
        ]
    }
]

现在,我想使用此代码将year_month 2020_02的状态更改为true,并且它可以工作:

testSchema.findOneAndUpdate(
    {
        _id: '5e90ae0e0ed9974174e92826',
        payments: { $elemMatch: { year_month: '2020_02' }}
    },
    { $set: {
        'payments.$': {
            year_month: '2020_02',
            status: false
        }
      }
    },
    {
        new: true,
        upsert: true
    }
).then( result => {
    response.send(result);
});

当我尝试这样做时出现问题

testSchema.findOneAndUpdate(
    {
        _id: '5e90ae0e0ed9974174e92826',
        payments: { $elemMatch: { year_month: '2020_03' }}
    },
    { $set: { 
        'payments.$': {
            year_month: '2020_03',
            status: false
        }
      },
    },
    {
        new: true,
        upsert: true
    }
).then( result => {
    response.send(result);
});

我是从upsert得到此消息的...

(node:8481) UnhandledPromiseRejectionWarning: MongoError: The positional operator did not find the match needed from the query.
    at Connection.<anonymous> (/home/vedran/Documents/Projekt/node_modules/mongodb/lib/core/connection/pool.js:466:61)
    at Connection.emit (events.js:223:5)
    at Connection.EventEmitter.emit (domain.js:475:20)
    at processMessage (/home/vedran/Documents/Projekt/node_modules/mongodb/lib/core/connection/connection.js:384:10)
    at TLSSocket.<anonymous> (/home/vedran/Documents/Projekt/node_modules/mongodb/lib/core/connection/connection.js:553:15)
    at TLSSocket.emit (events.js:223:5)
    at TLSSocket.EventEmitter.emit (domain.js:475:20)
    at addChunk (_stream_readable.js:309:12)
    at readableAddChunk (_stream_readable.js:290:11)
    at TLSSocket.Readable.push (_stream_readable.js:224:10)
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:181:23)
(node:8481) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:8481) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

根据文档Mongoose.findOneAndUpdate(),这应该可以工作,但是我犯了一些错误,我无法弄清楚到底是什么。 我知道匹配查询是问题所在,但我不确定如何更改它,以便应用upsert。

最后我像这样解决了它:

testSchema.findOneAndUpdate(
    {
        _id: '5e90ae0e0ed9974174e92826',
        payments: { $elemMatch: { year_month: '2020_03' }}
    },
    {
        $set: {
            'payments.$': {
                year_month: '2020_02',
                status: false
            }
        }
    },
    {new: true}
).then( success => {
            // response === null if no match is found
            if( success ) {
                response.send(success);
            } else {
                testSchema.findOneAndUpdate(
                    { _id: '5e90ae0e0ed9974174e92826' },
                    {
                        $push: {
                            'payments': request.body
                        }
                    },
                    {new: true}
                ).then(success => {
                    response.send(success);
                });
            }
        },
        error => {
            response.send(error);
        }
);

但是我在这里提出了两个请求,这可能会导致竞赛条件问题。 1.更新和 2.如果不存在则添加

我想知道是否有更好的方法可以使它使用upsert并避免出现竞争情况。

还有一个不错的简短tutorial on mongoose page,它描述了findOneAndUpdate的更新,但是它不包含数组,这可能会使我的问题复杂化。


基于joe和prasad_的答复的最终解决方案。 实际上,一旦您花时间了解这里发生的情况,事情就没那么复杂了。

testSchema.findOneAndUpdate(
    { "_id": customerId },
    [{
        $set: {
            payments: {
                $cond: [

                    {
                        $gt: [
                            {
                                $size: {
                                    $filter: {
                                        input: "$payments", 
                                        cond: {
                                            $eq: [
                                                "$$this.year_month",
                                                testData.payments.year_month
                                            ]
                                        }
                                    }
                                }
                            },
                            0
                        ]
                    },

                    {
                        $reduce: {
                            input: "$payments",
                            initialValue: [],
                            in: {
                                $concatArrays: [
                                    "$$value",
                                    [{
                                        $cond: [
                                            { $eq: ["$$this.year_month", testData.payments.year_month] },
                                            { $mergeObjects: ["$$this", { status: testData.payments.status }] },
                                            "$$this"
                                        ]
                                    }]
                                ]
                            }
                        }
                    },

                    {
                        $concatArrays: [
                            "$payments",
                            [testData.payments]
                        ]
                    }
                ]
            }
        }
    }],
    { new: true }
).then( 
    success => {
        response.send(success);
    },
    error => {
        response.send(error);
    }
);

2 个答案:

答案 0 :(得分:1)

主要问题是findOneAndUpdate确实按照其名称所暗示的那样工作。它使用提供的过滤器执行find,如果找到匹配项,则将更新应用于第一个匹配的文档。

如果集合仅包含此文档:

[
    {
        "_id": "5e90ae0e0ed9974174e92826",
        "payments": [
            {
                "year_month": "2020_02",
                "status": false
            }
        ]
    }
]

最初的查找部分基本上是

.find({
        _id: '5e90ae0e0ed9974174e92826',
        payments: { $elemMatch: { year_month: '2020_03' }}
})

这不匹配任何内容,并且由于upsert设置为true,因此fineOneAndUpdate尝试创建全新的文档。即使它能够通过不匹配的位置运算符创建数组,它要添加的文档也会是:

 {
        "_id": "5e90ae0e0ed9974174e92826",
        "payments": [
            {
                "year_month": "2020_03",
                "status": false
            }
        ]
}

这是不正确的,并且由于重复的_id值而无法插入。

如果您使用的是MongoDB 4.2,则可以使用聚合管道作为findAndUpdate的第二个参数来检查数组中是否有您感兴趣的元素,并在缺少该元素时添加它。

下面是一种不太漂亮的方法。 findOneAndUpdate将与_id匹配,并且管道将:
-检查数组中的任何元素是否与所需的year_month
相匹配 -如果是这样,则$ reduce数组以更新该元素中的状态字段
-如果没有,请添加一个新元素
-将结果分配回payments

.findOneAndUpdate(
    { "_id": "5e90ae0e0ed9974174e92826" },
    [{$set: {
         payments: {$cond:[
                 {$gt:[
                       {$size:
                             {$filter:{
                                  input:"$payments", 
                                  cond:{$eq:["$$this.year_month","2020_03"]}
                       }}},
                       1
                  ]},
                  {$reduce:{
                        input:"$payments",
                        initialValue:[],
                        in:{$concatArrays:[
                                  "$$value",
                                  [{$cond:[
                                       {$eq:["$$this.j",3]},
                                       {$mergeObjects:["$$this",{status:true}]},
                                       "$$this"
                                  ]}]
                        ]}
                  }},
                  {$concatArrays:[
                       "$payments",
                       [{year_month:"2020_03", status:true}]
                  ]}
          ]}
     }}]
)

答案 1 :(得分:1)

考虑两个输入文件。将插入第一个(year_month: '2020_03'数组中不存在payments)。使用第二个更新运行更新时,它将更新数组中现有子文档的status

更新操作仅对MongoDB 4.2或更高版本有效,因为它使用管道进行更新。

INPUT_DOC = { year_month: '2020_03', status: false }    // this will get inserted into the array
INPUT_DOC = { year_month: '2020_02', status: true }     // updates the sub-document

db.collection.findOneAndUpdate(
  { 
      _id: "5e90ae0e0ed9974174e92826" 
  },
  [ 
      { 
          $set: { 
              payments: {
                  $reduce: {
                      input: "$payments", 
                      initialValue: { payments: [], update: false },
                      in: {
                          $cond: [ { $eq: [ "$$this.year_month", INPUT_DOC.year_month ] },
                                   { 
                                      payments: { 
                                          $concatArrays: [
                                               [ { _id: "$$this._id", year_month: "$$this.year_month", status: INPUT_DOC.status } ],
                                               "$$value.payments"
                                           ] 
                                      }, 
                                      update: true
                                   },
                                   { 
                                      payments: { 
                                          $concatArrays: [  [ "$$this" ], "$$value.payments" ] 
                                      }, 
                                      update: "$$value.update" 
                                   }
                          ]
                      }
                  }
              }
          }
      },
      { 
          $set: { 
              payments: { 
                  $cond: [ { $eq: [ "$payments.update", false ] },
                           { $concatArrays: [ [ INPUT_DOC ], "$payments.payments" ] },
                           { $concatArrays: [ [ ], "$payments.payments" ] }
                  ] 
              }
          }
      }
  ],
  { 
      new: true, 
      upsert: true 
  }
)