binarygiant
binarygiant

Reputation: 6422

Mongoose findOneAndUpdate Upsert _id null?

I'd like to have a single method that either creates or updates a document for a policy. Searching and trying different techniques like this one, I have come up with a null _id for my document. Using findByIdAndUpdate has a similar affect.

I see a document inserted in the collection, but the _id field is null:

exports.savePolicy = function (plcy, callback) {
    console.log('priority is : ' + plcy.priority)
    try {
        var policy = new Policy(plcy);
        var query = {_id: plcy._id};  //this may be null
        var update = {
            name: plcy.name || defaults.policyDefaults.name,
            longDescription: plcy.longDescription || defaults.policyDefaults.longDescription,
            shortDescription: plcy.shortDescription || defaults.policyDefaults.shortDescription,
            priority: plcy.priority, colorHex: plcy.colorHex || defaults.policyDefaults.colorHex,
            settings: plcy.settings || [],
            parentPolicyId: plcy.parentPolicyId || null
        }

        Policy.findOneAndUpdate(query, update, {upsert: true}, function (err, data) {
            callback(err, data);
        });

    } catch (e) {
        log.error('Exception while trying to save policy: ' + e.message);
        callback(e, null);
    }

Is there something that can be done to get the _id not to be null when its not an update?

Upvotes: 32

Views: 26903

Answers (9)

Bruce C
Bruce C

Reputation: 111

The easiest way to update document if there's an ID or create a new document if there's no ID is:

Model.findOneAndUpdate({ _id: id ?? new mongoose.Types.ObjectId() }, updates, { upsert: true });

Note the use of the nullish coalescing operator (??) which allows 0 as a valid id. The updates object doesn't need to contain an id property, as mongoose will add it automatically:

if no document matches filter, MongoDB will insert one by combining filter and update as shown below.

Upvotes: 5

Izhaki
Izhaki

Reputation: 23586

This could be useful (from the docs):

If you specify an _id field in either the query parameter or replacement document, MongoDB uses that _id field in the inserted document.

So when you write:

model.findOneAndUpdate({_id: id}, data, f);

And id is null, it will insert a document with null as id.

Upvotes: 1

jacobedawson
jacobedawson

Reputation: 3212

Since we can now add default properties while destructuring objects in JavaScript, in the case that we are checking to see if a document exists, I find this the easiest way to either query via an existing _id or create a new one in the same operation, avoiding the null id problem:

// someController.js using a POST route

async function someController(req, res) {
  try {
  const {
    // the default property will only be used if the _id doesn't exist
    _id: new mongoose.Types.ObjectId(),
    otherProp,
    anotherProp
  } = req.body;
  const createdOrUpdatedDoc = await SomeModel.findOneAndUpdate(
    {
      _id
    },
    {
      otherProp,
      anotherProp
    },
    {
      new: true,
      upsert: true
    }
  ).exec();
  return res.json(201).json(createdOrUpdatedDoc);
  } catch (error) {
    return res.json(400).send({
      error,
      message: "Could not do this"
    })
  }
}

Upvotes: 0

TomoMiha
TomoMiha

Reputation: 1279

Try setting 'new' parameter to true:

{ upsert: true, new: true }

More info: https://github.com/Automattic/mongoose/issues/2939

Upvotes: -2

sarora
sarora

Reputation: 914

Thanks to JohnnyHK for a helpful answer above. I came up with a one-liner since it's used so frequently:

query = args._id ? { _id: args._id } : { _id: new ObjectId() };

It relies on the following require:

const ObjectId = require('mongodb').ObjectID;

Upvotes: 3

m4js7er
m4js7er

Reputation: 117

I am not using Mongoose, but I run into similar issue with MongoDB. For Upsert action when new object was inserted the MongoDB was setting null to _id.

I was calling:

findOneAndUpdate({my_id:'my_unique_id'}, obj, {upsert: true})

where obj._id was undefined.

The problem was that _id was present on list of keys Object.keys(obj). I found that I was assigning obj._id = some_variable, where some_variable was undefined, and that was causing _id to appear on a list of keys.

I applied workaround by calling right before upsert:

if (_.isUndefined(obj._id)) {
  delete obj._id;
}

Upvotes: 1

JohnnyHK
JohnnyHK

Reputation: 312095

null is a valid _id value in MongoDB, so if you don't want it used in new docs you must ensure that a null value is replaced with a new ObjectID in query:

var query = {_id: plcy._id};
if (!query._id) {
    query._id = new mongoose.mongo.ObjectID();
}

// the rest stays the same...

Upvotes: 25

Trent
Trent

Reputation: 1381

I had this same problem and couldn't figure out a way to get it to work. I ended up writing my own upsert method like so....

var upsert = function(model, data, f){
  if (!data._id) {
    model.create(data, f);
  } else {
    var id = data._id;
    delete data._id;
    model.findOneAndUpdate({_id: id}, data, f);
  }
}

That allows me to call it for any of my models with one line of code...

upsert(Team, team, f);

There might be a better way to do it, but this is working for me. I can do updates and inserts and I don't get a null _id on insert.

Upvotes: 6

Jeff Tsui
Jeff Tsui

Reputation: 1276

Try setting the upsert parameter to true in your call to update. From the mongoid docs: http://docs.mongodb.org/manual/reference/method/db.collection.update/#update-parameter

Optional. If set to true, creates a new document when no document matches the query criteria. The default value is false, which does not insert a new document when no match is found.

Upvotes: -3

Related Questions