MongoDB update in array fails: Updating the path 'companies.$.updatedAt' would create a conflict at 'companies.$'

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 ... :-(

Upvotes: 7

Views: 2719

Answers (1)

Tom Slabbaert
Tom Slabbaert

Reputation: 22296

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.

Upvotes: 5

Related Questions