7

we upgraded (from MongoDB 3.4) to:

MongoDB: 4.2.8

Mongoose: 5.9.10

and now we receive those errors. For the smallest example the models are:

[company.js]

'use strict';

const Schema = require('mongoose').Schema;

module.exports = new Schema({
  name: {type: String, required: true},
}, {timestamps: true});

and [target_group.js]

'use strict';

const Schema = require('mongoose').Schema;

module.exports = new Schema({
  title: {
    type: String,
    required: true,
    index: true,
  },
  minAge: Number,
  maxAge: Number,
  companies: [Company],
}, {timestamps: true});

and when I try to update the company within a targetgroup

  _updateTargetGroup(companyId, company) {
    return this.targetGroup.update(
      {'companies._id': companyId},
      {$set: {'companies.$': company}},
      {multi: true});
  }

I receive

MongoError: Updating the path 'companies.$.updatedAt' would create a conflict at 'companies.$'

even if I prepend

    delete company.updatedAt;
    delete company.createdAt;

I get this error.

If I try similar a DB Tool (Robo3T) everything works fine:

db.getCollection('targetgroups').update(
  {'companies.name': "Test 1"},
  {$set: {'companies.$': {name: "Test 2"}}},
  {multi: true});

Of course I could use the workaround

  _updateTargetGroup(companyId, company) {
    return this.targetGroup.update(
      {'companies._id': companyId},
      {$set: {'companies.$.name': company.name}},
      {multi: true});
  }

(this is working in deed), but I'd like to understand the problem and we have also bigger models in the project with same issue.

Is this a problem of the {timestamps: true}? I searched for an explanation but werenot able to find anything ... :-(

2 Answers2

5

The issue originates from using the timestamps as you mentioned but I would not call it a "bug" as in this instance I could argue it's working as intended.

First let's understand what using timestamps does in code, here is a code sample of what mongoose does to an array (company array) with timestamps: (source)

  for (let i = 0; i < len; ++i) {
    if (updatedAt != null) {
      arr[i][updatedAt] = now;
    }
    if (createdAt != null) {
      arr[i][createdAt] = now;
    }
  }

This runs on every update/insert. As you can see it sets the updatedAt and createdAt of each object in the array meaning the update Object changes from:

{$set: {'companies.$.name': company.name}}

To:

{
  "$set": {
    "companies.$": company.name,
    "updatedAt": "2020-09-22T06:02:11.228Z", //now
    "companies.$.updatedAt": "2020-09-22T06:02:11.228Z" //now
  },
  "$setOnInsert": {
    "createdAt": "2020-09-22T06:02:11.228Z" //now
  }
}

Now the error occurs when you try to update the same field with two different values/operations, for example if you were to $set and $unset the same field in the same update Mongo does not what to do hence it throws the error.

In your case it happens due to the companies.$.updatedAt field. Because you're updating the entire object at companies.$, that means you are basically setting it to be {name: "Test 2"} this also means you are "deleting" the updatedAt field (amongst others) while mongoose is trying to set it to be it's own value thus causing the error. This is also why your change to companies.$.name works as you would only be setting the name field and not the entire object so there's no conflict created.

Tom Slabbaert
  • 21,288
  • 10
  • 30
  • 43
  • Ok, many thanks for this explanation. So my next question is, if there is a way to use "updating an entire object at companies.$" (while keeping timestamps) or if this doesn't work at all in recent mongodb/mongoose. – Rüdiger Krauße Sep 22 '20 at 07:41
  • 1
    No you can't really as this "feature" will keep happening. but the real question is why do you want to update the entire object, doing so will delete all these fields like `_id`, `updatedAt` etc. if you don't care about these fields to begin with why add the `timestamp` option to the schema? – Tom Slabbaert Sep 22 '20 at 08:09
  • Ok, I understand. So I will start to rework the code... Best regards, Rüdiger – Rüdiger Krauße Sep 22 '20 at 08:45
  • @RüdigerKrauße Did you later resolve this issue?? – Peoray Oct 25 '21 at 20:09
-1

Adding a new answer.

If you have a large document that you want to update and not hand type all of the set values, you can use this.

delete company._id;
delete company.updatedAt;
delete company.createdAt;

const setObjects = {};
for (const [key, value] of Object.entries(company)) {
  setObjects[`companies.$.${key}`] = value;
}

await Companies.updateOne(
  { companies._id: companyId },
  {
    $set: setObjects,
  }
);

We are basically creating an object with all the values that you want to set. Just make sure you delete _id, createdAt, and updatedAt

Alan Spurlock
  • 343
  • 2
  • 10