33

I have

Page.findById(pageId).then(page => {
  const pageId = page.id;
   ..
});

My problem is that if no page id is given, it should just take the first available page given some conditions, which is done by

Page.findOne({}).then(page => {
  const pageId = page.id;
  ..
});

but if no page is found, it should create a new page and use this, which is done with

Page.create({}).then(page => {
  const pageId = page.id;
  ..
});

But how do I combine all this to as few lines as possible?

I have a lot of logic going on inside

page => { ... }

so I would very much like to do this smart, so I can avoid doing it like this

if (pageId) {
  Page.findById(pageId).then(page => {
    const pageId = page.id;
     ..
  });
} else {
  Page.findOne({}).then(page => {
    if (page) {
      const pageId = page.id;
      ..
    } else {
      Page.create({}).then(page => {
        const pageId = page.id;
        ..
      });
    }
  });
}

I am thinking I maybe could assign a static to the schema with something like

pageSchema.statics.findOneOrCreate = function (condition, doc, callback) {
  const self = this;
  self.findOne(condition).then(callback).catch((err, result) => {
    self.create(doc).then(callback);
  });
};
Jamgreen
  • 10,329
  • 29
  • 113
  • 224

8 Answers8

57

As per the Mongoose docs:

As per previous SO answer

Model.findByIdAndUpdate()

"Finds a matching document, updates it according to the update arg, passing any options, and returns the found document (if any) to the callback."

In the options set upsert to true:

upsert: bool - creates the object if it doesn't exist. defaults to false.

Model.findByIdAndUpdate(id, { $set: { name: 'SOME_VALUE' }}, { upsert: true  }, callback)
Julian Boyce
  • 731
  • 1
  • 6
  • 5
  • 14
    If you don't have the id, findOneAndUpdate() also has the same upsert option: https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate – ahaurat Aug 21 '18 at 05:11
  • 2
    Here's a good [comparison table of Mongoose update operations](https://masteringjs.io/tutorials/mongoose/update), highlighting atomic ones. – Dan Dascalescu May 17 '20 at 03:12
17

Related to Yosvel Quintero's answer which didn't work for me:

pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, callback) {
    const self = this
    self.findOne(condition, (err, result) => {
        return result ? callback(err, result) : self.create(condition, (err, result) => { return callback(err, result) })
    })
}

And then use it like:

Page.findOneOrCreate({ key: 'value' }, (err, page) => {
    // ... code
    console.log(page)
})
David Joos
  • 926
  • 11
  • 16
13

Promise async/await version.

Page.static('findOneOrCreate', async function findOneOrCreate(condition, doc) {
  const one = await this.findOne(condition);

  return one || this.create(doc);
});

Usage

Page.findOneOrCreate({ id: page.id }, page).then(...).catch(...)

Or

async () => {
  const yourPage = await Page.findOneOrCreate({  id: page.id }, page);
}
ninhjs.dev
  • 7,203
  • 1
  • 49
  • 35
  • This is not atomic. – Dan Dascalescu May 17 '20 at 03:07
  • If more than one client write to the collection at the same time, it's possible that right after the `.findOne()` returns false, another client creates a document, then this code creates another one. See [atomicity](https://mongoosejs.com/docs/tutorials/findoneandupdate.html#atomic-updates). – Dan Dascalescu May 17 '20 at 06:33
  • When does that become truth? I believe that if you have a good system design, your developers won't need to deal with that. If `findOne` is actually not good in many cases, I believe mongoose/mongodb would mark it deprecated. For simple, just think about above codes as a shortcut to `find or create`. Or using `upsert` as another answer mentioned. – ninhjs.dev May 17 '20 at 16:47
  • No "good system design" can prevent the situation I've described. Sure, it's unlikely to happen in small developments, with rare writes. A good question to ask is, what really are the benefits of using the two-step method in this answer, vs. the single, atomic, step in [the findOneAndUpdate answer](https://stackoverflow.com/questions/40102372/find-one-or-create-with-mongoose/50648255#50648255). – Dan Dascalescu May 18 '20 at 01:20
10

Each Schema can define instance and static methods for its model. Statics are pretty much the same as methods but allow for defining functions that exist directly on your Model

Static method findOneOrCreate:

pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, doc, callback) {
  const self = this;
  self.findOne(condition, (err, result) => {
    return result 
      ? callback(err, result)
      : self.create(doc, (err, result) => {
        return callback(err, result);
      });
  });
};

Now when you have an instance of Page you can call findOneOrCreate:

Page.findOneOrCreate({id: 'somePageId'}, (err, page) => {
  console.log(page);
});
Yosvel Quintero
  • 18,669
  • 5
  • 37
  • 46
3

One lines solution with async/await:

const page = Page.findOne({}).then(p => p || p.create({})

Pooya
  • 862
  • 7
  • 10
0

If you don't want to add a static method to the model, you can try to move some things around and at least not to have all these callback nested levels:

function getPageById (callback) {
  Page.findById(pageId).then(page => {
    return callback(null, page);
  });
}

function getFirstPage(callback) {
  Page.findOne({}).then(page => {
    if (page) {
      return callback(null, page);
    }

    return callback();
  });
}

let retrievePage = getFirstPage;
if (pageId) {
  retrievePage = getPageById;
}

retrievePage(function (err, page) {
  if (err) {
    // @todo: handle the error
  }

  if (page && page.id) {
    pageId = page.id;
  } else {
    Page.create({}).then(page => {
      pageId = page.id;
    });
  }
});
Stavros Zavrakas
  • 3,045
  • 1
  • 17
  • 30
0

The solutions posted here ignore that this pattern is most common when there's a unique index on a field or a combination of fields. This solution considers unique index violation errors correctly:

mongoose.plugin((schema) => {
  schema.statics.findOrCreate = async function findOrCreate(key, attrs) {
    try {
      return await this.create({ ...attrs, ...key });
    } catch (error) {
      const isDuplicateOnThisKey =
        error.code === 11000 &&
        Object.keys(error.keyPattern).sort().join(',') ===
          Object.keys(key).sort().join(',');
      if (isDuplicateOnThisKey) {
        const doc = await this.findOne(error.keyValue);
        doc.set(attrs);
        return await doc.save();
      }
      throw error;
    }
  };
});

Usage:

await Post.findOrCreate({ slug: 'foobar' }, { title: 'Foo Bar', body });
djanowski
  • 5,610
  • 1
  • 27
  • 17
-1

try this..

 var myfunc = function (pageId) {
  // check for pageId passed or not
 var newId = (typeof pageId == 'undefined') ? {} : {_id:pageId};

 Page.findOne(pageId).then(page => {
 if (page)
 const pageId = page.id;
 else {  // if record not found, create new

    Page.create({}).then(page => {
        const pageId = page.id;
    });
  }
});

 }
Umakant Mane
  • 1,001
  • 1
  • 8
  • 9