0

I'm trying to figure out how to update a collection in my MongoDB. Basically, I want to take a list of email addresses, check if they exist in the DB, and if they don't, create a new User from them, all in the same query. Is this possible?

Right now I'm doing this:

const usersWithEmailsOnly = [<array-of-emails];

User.update({
   email: {
      $in: usersWithEmailsOnly.map(userWithEmail => userWithEmail.email),
   },
}, { multi: true, upsert: true }).exec()

And I get this response back from the server:

{ ok: 0, n: 0, nModified: 0 }

And when I look at the DB, I see that no users are created. Is there something else I need to be doing?

Stevie Star
  • 2,331
  • 2
  • 28
  • 54
  • 1
    Possible duplicate of [How do I update/upsert a document in Mongoose?](https://stackoverflow.com/questions/7267102/how-do-i-update-upsert-a-document-in-mongoose) – Mani Mar 04 '19 at 19:38
  • I've looked at that one but it doesn't seem to discuss handling multiple documents at once. Is this possible? – Stevie Star Mar 04 '19 at 20:26
  • mongo has db.coll.updateMany (https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/#db.collection.updateMany) but I'm unsure what that matches up with in mongoose – klhr Mar 04 '19 at 20:39
  • Based on your response back from server, it’s looks like no documents inserted. – Mani Mar 04 '19 at 20:39

1 Answers1

2

You want bulkWrite()

Users.bulkWrite(
  usersWithEmailsOnly.map(userWithEmail =>
    ({ 
      "updateOne": {
        "filter": { "email": userWithEmail.email },
        "update": { "$setOnInsert": { "name": userWithEmail.name } },
        "upsert": true
      }
    })
  )
)

This couples with the $setOnInsert update operator which has the special condition that it's specified arguments only update the document when an actual "upsert/insert" happens. So an existing document that is matched simply receives no changes when this is the only operator used.

Note that "upserts" will always apply the criteria used in the query/predicate part of the update statement, as long as the expression equates to a singular value; i.e age: { $gt: 3 } would not be singular. For most "data loading" you would typically want creation of a static predicate as the "unique key" and therefore selector for the update as well as some other data, which would be where you apply a $setOnInsert as needed.

Technically it is still multiple updates but unlike attempting to loop a series of updates in code this is a single request and response to and from the server. It pretty much has the same responsiveness of a single write ( of course with the payload overhead ) from the point of view of your application.

There is no such thing as a true "single statement" as differing criteria for the data to update cannot be applied to multiple documents in a MongoDB update. You can apply the same data to update over multiple documents, but when you want "this condition to match a document also means I update with this specific data when that happens", then that is what you use bulkWrite() for.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • This is super informative. Thankyou so much this worked exactly for what I needed :D You are a saint and you deserve much praise – Stevie Star Mar 04 '19 at 21:33
  • Is there any way to change this so that if the email DOES exist, it runs an `{ $addToSet: { events: eventId }}` operation and only updates that one specific field? – Stevie Star Mar 04 '19 at 21:51
  • @StevieStar Yes. `$addToSet` would be applied both on match or on upsert. All other [update operators](https://docs.mongodb.com/manual/reference/operator/update/) with the **exception** of `$setOnInsert` basically apply to every match *including* upserts. – Neil Lunn Mar 04 '19 at 21:59
  • Nice :D So would the update syntax end up looking like ```update: { $addToSet: { myEvents: req.body.eventId }, $setOnInsert: { email: user.email }, },```? – Stevie Star Mar 04 '19 at 22:48
  • @StevieStar The `$setOnInsert` is not required for `email` because that's already in the **predicate**. That's what I explained in the question. You only use it for fields not in the predicate that you want **only** on insertion. i.e We are attempting to match on `email`, so if nothing is found that value is automatically inserted. – Neil Lunn Mar 05 '19 at 00:02