пн goose обновить массив или добавить в массив - PullRequest
0 голосов
/ 15 апреля 2020

Я уже некоторое время пытаюсь запустить это, но не могу понять, что я делаю неправильно.

У меня есть две схемы, подобные этой

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.

Согласно документации Пн goose .findOneAndUpdate () это должно работать, но я делаю некоторую ошибку, и я не могу понять, что именно. Я знаю, что запрос на совпадение - это проблема, но я не уверен, как изменить его, чтобы применить апсерт.

В конце я решил это так:

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 и избегать условий гонки.

Существует также хорошее короткое руководство по mon goose page , в котором описывается upsert для findOneAndUpdate, но оно не включает массивы, и это, вероятно, и осложняет проблему в моем случае.


Окончательное решение, основанное на ответах Джо и Прасада. На самом деле все не так сложно, если вы потратите время, чтобы понять, что здесь происходит.

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 ]

1 голос
/ 16 апреля 2020

Рассмотрим два входных документа. Первый будет вставлен (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 
  }
)
1 голос
/ 16 апреля 2020

Основная проблема в том, что 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
- Если это так, $ уменьшить массив, чтобы обновить поле состояния в этом элементе
- Если нет, добавьте новый элемент
- присвойте результат обратно 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}]
                  ]}
          ]}
     }}]
)
...