0

I am connecting to the Yelp API using the RapidAPI module in Nodejs. I am able to request a token, connect, and request data, retrieve that data, and insert the relevant information for each result it into mongodb. Here's where it gets complicated...

Let's say I make a Yelp API request and search for bars. I get a list of bars and insert them into the database. Let's say one of these in the list is "Joe's Bar & Grill". One of the fields in my mongodb is "type" and it's an array. So now, this particular document will look something like this:

{
  id: 'joes-bar-and-grill',
  name: 'Joe\'s Bar & Grill',
  type: ['bar']
}

But then I run another request on the Yelp API on "restaurants", and in this list "Joe's Bar & Grill" shows up again. Instead of inserting a new duplicate document into mongodb, I'd like the existing document to end up looking like this:

{
  id: 'joes-bar-and-grill',
  name: 'Joe\'s Bar & Grill',
  type: ['bar', 'restaurant']
}

In addition to this, let's say I run another request again for "bars", and "Joe's Bar & Grill" comes up again. I don't want it to automatically insert "bar" into the type array again, if "bar" already exists in its array.

I've tried findOneAndUpdate with upsert: true and a $push of new data into the array, but I cannot get it to work at all. Does anyone have any ideas?

Armin
  • 1,736
  • 4
  • 19
  • 35
  • You are using the wrong operator. What you want is the `$addToSet` operator – styvane Apr 04 '17 at 10:54
  • Do you know which MongoDB version you are using? There are some new and different update-style methods in the newer versions, so we need to take that into account when answering. – Vince Bowdren Apr 04 '17 at 16:45

2 Answers2

1

You can use findOneAndUpdate, combined with $addToSet (to make sure that an entry in the array only exists once) and $each (to allow passing arrays to $addToSet):

Bar.findOneAndUpdate({ id : 'joes-bar-and-grill' }, {
  id        : 'joes-bar-and-grill',
  name      : 'Joe\'s Bar & Grill',
  $addToSet : { type : { $each : [ 'restaurant' ] } }
}, { upsert : true })

EDIT: now that you posted your entire code, the problem becomes more obvious.

For one, I'm not sure if the third and fourth arguments that you're passing to Location.update() make sense. As far as I know, the third should be an option object, and the fourth an async function.

Secondly, it looks like you're just ignoring any update errors.

And lastly, this isn't going to work:

for (var i = 0; i < payload.businesses.length; i++) { Location.update(...) }

Because Location.update() is asynchronous, the i variable will get clobbered (you should browse around on SO to find the explanation for that; for example, see this question).

You're going to need a library that will provide you with better async support, and preferably one that will also help limiting the number of update queries.

Once such library is async, and using it, your code would become something like this:

const async = require('async');

...

async.eachLimit(payload.businesses, 5, function(business, callback) {
  Location.update({ yelpID : business.id }, {
    name      : business.name,
    latitude  : business.location.latitude,
    longitude : business.location.longitude,
    address1  : business.location.address1,
    address2  : business.location.address2,
    address3  : business.location.address3,
    city      : business.location.city,
    state     : business.location.state,
    zip_code  : business.location.zip_code,
    country   : business.location.country,
    timezone  : 'CST'
    $addToSet : { type : 'bar' }
  }, { upsert : true }, callback);
}, function(err) {
  if (err) {
    console.error(err);
  } else {
    console.log('All documents inserted');
  }
});
Community
  • 1
  • 1
robertklep
  • 198,204
  • 35
  • 394
  • 381
  • I don't think this will work as written, because your [update parameter](https://docs.mongodb.com/v3.2/reference/method/db.collection.findOneAndUpdate/) contains both field names and an $addToSet operator; but it's only supposed to contain update operators. Did you mean to put the _id_ and _name_ sections in a $set section? – Vince Bowdren Apr 04 '17 at 16:39
  • @VinceBowdren Mongoose does that automatically (documented [here](http://mongoosejs.com/docs/api.html#model_Model.update): _"All top level keys which are not atomic operation names are treated as set operations"_). Also, I actually tested this code ;D – robertklep Apr 04 '17 at 17:21
  • Fair enough so, I wasn't aware of Mongoose using a different syntax. – Vince Bowdren Apr 05 '17 at 08:15
  • I attempted this but nothing is being inserted into the database. No errors, either @robertklep – Armin Apr 07 '17 at 03:36
  • @robertklep that worked!! i had done some research on async and i figured it might have something to do with the issue. my background is predominantly php/mysql so I don't have much experience in changing from procedural code, but this has been extremely helpful and informative! thanks you so much! – Armin Apr 07 '17 at 07:56
-1

You may use $addToSet operator

The $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array.

$addToSet only ensures that there are no duplicate items added to the set and does not affect existing duplicate elements. $addToSet does not guarantee a particular ordering of elements in the modified set.

If the field is absent in the document to update, $addToSet creates the array field with the specified value as its element.

If the field is not an array, the operation will fail.

The below solution assumes that on each update, you receive a single type and not an array. If the input document is an array itself, you may use robertklep's solution with $each operator

db.mycoll.update(
   { "id" : "joes-bar-and-grill" },
   {
      $set:{
           name : 'Joe\'s Bar & Grill',
      },
      $addToSet : { type : 'restaurant' }
   },
   true, false);

I have also used $set operator.

The $set operator replaces the value of a field with the specified value.

The $set operator expression has the following form:

{ $set: { field1: value1, ... } }

Here is the mongo shell output to explain it further :

> db.mycoll.find({ "id" : "joes-bar-and-grill" });
  // NO RESULT

> db.mycoll.update(
...    { "id" : "joes-bar-and-grill" },
...    {
...       $set:{
...            name : 'Joe\'s Bar & Grill',
...       },
...       $addToSet : { type : 'restaurant' }
...    },
...    true, false);
WriteResult({
    "nMatched" : 0,
    "nUpserted" : 1,
    "nModified" : 0,
    "_id" : ObjectId("58e719b4d543c5e30d615d59")
})
 // INSERTED A NEW DOCUMENT AS IT DOES NOT EXIST

> db.mycoll.find({ "id" : "joes-bar-and-grill" }); // FINDING THE OBJECT
{ "_id" : ObjectId("58e719b4d543c5e30d615d59"), "id" : "joes-bar-and-grill", "name" : "Joe's Bar & Grill", "type" : [ "restaurant" ] }


> db.mycoll.update(
...    { "id" : "joes-bar-and-grill" },
...    {
...       $set:{
...            name : 'Joe\'s Bar & Grill',
...       },
...       $addToSet : { type : 'bar' }
...    },
...    true, false);
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
// UPDATING THE DOCUMENT WITH NEW TYPE : "bar"

> db.mycoll.findOne({ "id" : "joes-bar-and-grill" });
{
    "_id" : ObjectId("58e719b4d543c5e30d615d59"),
    "id" : "joes-bar-and-grill",
    "name" : "Joe's Bar & Grill",
    "type" : [
        "restaurant",
        "bar"
    ]
}
Rahul
  • 15,979
  • 4
  • 42
  • 63
  • I don't think you can nest update operators like that; you've got $addToSet contained within the $set section. – Vince Bowdren Apr 04 '17 at 16:42
  • @VinceBowdren Thanks for informing. Fixed now. – Rahul Apr 05 '17 at 00:00
  • but this only updates an existing document, correct? what i need is something that will update if it exists, and insert if it does not. – Armin Apr 07 '17 at 03:54
  • @robotsushi it will update if it exists and insert a new document if not already exists. Please try it in your Mongo shell – Rahul Apr 07 '17 at 04:44
  • thanks for the quick reply! I am using this in my script as a mongoose Model. I have updated this post with my code for your review. When I attempt the code you provided I get an error which will also be listed above. – Armin Apr 07 '17 at 05:10