Easier way to Update an Array with MongoDB

蓝咒 提交于 2019-12-24 15:24:14

问题


My mongoose collection looks something like:

var followSchema = new Schema({
  facebookId: {type: String, required: true},
  players : [],
  fans: [],
});

When a player wants to follow another user, I add that user's id into the players[] array.

In order to achieve this I first look up the player's record:

var myRecord = FollowModel.findOneAndUpdate(
      {facebookId: req.user.facebookId},
      {$setOnInsert: {}},
      {upsert: true, new : true}
    );

The above ensures that if the player doesn't exist, one is created.

Then I go about checking the 'players[]' array:

myRecord.exec(function(err, result) {
  if (err) {
    throw err;
  }

  if (result.players.indexOf(req.body.idToFollow) < 0) {
    result.players.push(req.body.idToFollow);
    result.save();
    res.status(200).send('Player added to Follow List');
  } else {
    res.status(200).send('Already Following this player');
  }
});

I was just wondering if there is a more straight-forward and lucid way of writing this query ?


回答1:


If you "do care" about adding a bit more functionality here ( very much advised ) and limiting the overhead of updates where you really do no need to return the modified document, or even if you do then is is always better to use atomic operators with arrays like $push and $addToSet.

The "additional functionality" is also in that when using arrays in storage, it is a really wise practice to store the "length" or "count" of items. This becomes useful in queries and can be accessed efficiently with an "index", as opposed to other methods of getting the "count" of an array or using that "count/length" for filtering purposes.

The better construct here is to use "Bulk" operations as testing for array elements present does not mix well with the concept of "upserts", so where you want upsert functionality an array testing it's better in two operations. But since "Bulk" operations can be sent to the server with "one request" and you also get "one response", then this mitigates any real overhead in doing so.

var bulk = FollowModel.collection.initializeOrderedBulkOp();

// Try to add where not found in array
bulk.find({ 
    "facebookId": req.user.facebookId,
    "players": { "$ne": req.body.idToFollow }
}).updateOne({
    "$push": { "players": req.body.idToFollow },
    "$inc": { "playerCount": 1 }
});

// Otherwise create the document if not matched
bulk.find({
    "facebookId": req.user.facebookId,
}).upsert().updateOne({
    "$setOnInsert": {
        "players": [req.body.idToFollow]
        "playerCount": 1,
        "fans": [],
        "fanCount": 0
    }
})

bulk.execute(function(err,result) {
    // Handling in here
});

The way this works is that the first attempt there tries to find a document where the array element to be added is not present already within the array. No attempt is made at an "upsert" here as you do not want to create a new document if the only reason it did not match a document is becasuse the array element is not present. But where matched, then the new member is added to the array and the current "count" is "incremented" by 1 via $inc, which keeps the total count or length.

The second statement is therefore going to match the document only, and therefore does use an "upsert" since if the document is not found for the key field then it will be created. As all operations are inside $setOnInsert then there will be no operation performed if the document already exists.

It's all just really one server request and response, so there is no "back and forth" for the inclusion of two update operations, and that makes this efficient.

Removing an array entry is basically the reverse, except that this time there is no need to "create" a new document if it was not found:

var bulk = FollowModel.collection.initializeOrderedBulkOp();

// Try to remove where found in array
bulk.find({ 
    "facebookId": req.user.facebookId,
    "players": req.body.idToFollow
}).updateOne({
     "$pull": { "players": req.body.idToFollow },
     "$inc": { "playerCount": -1 }
});

bulk.execute(function(err,result) {
    // Handling in here
});

So now you only need to test where the array element is present and where it is then $pull the matched element from the array content, at the same time as "decrementing" the "count" by 1 to reflect the removal.

Now you "could" use $addToSet instead here as it will just look at the array content and if the member is not found then it will be added, and by much the same reasons there is no need to test for the array element existing when using $pull as it will just do nothing if the element is not there. Futhermore $addToSet in that context can be used directly within an "upsert", as long as you do not "cross paths" since it is not allowed to try and use multiple update operators on the same path with MongoDB:

FollowModel.update(
    { "facebookId": req.user.facebookId },
    {
        "$setOnInsert": {
            "fans": []
        },
        "$addToSet": { "players": req.body.idToFollow }
    },
    { "upsert": true },
    function(err,numAffected) {
        // handling in here
    }
);

But this would be "wrong":

FollowModel.update(
    { "facebookId": req.user.facebookId },
    {
        "$setOnInsert": {
            "players": [],              // <-- This is a conflict
            "fans": []
        },
        "$addToSet": { "players": req.body.idToFollow }
    },
    { "upsert": true },
    function(err,numAffected) {
        // handling in here
    }
);

However, by doing that you loose the "count" functionality since such operations are just completing with no regard to what is actually there or if anything was "added" or "removed".

Keeping "counters" is a really good thing, and even if you do not have an immediate use for them right now, then at some stage in the lifecyle of your application you are probably going to want them. So it makes a lot of sense to understand the logic involved and implement them now. Small price to pay now for a lot of benefit later.


Quick sidenote here as I generally recommend "Bulk" operations where possible. When using this via the .collection accessor in mongoose, then you need to be aware that these are native driver methods and therefore behave differently than the "mongoose" methods.

Notably, all "mongoose" methods have a buit-in "check" to see that the connection to the database is currently active. Where it is not, the operation is effectively "queued" until the connection is made. Using the native methods this "check" is no longer present. Therefore you either need to be sure that a connection is already present from a "mongoose" method having executed "first", or alternately wrap you whole application logic in a construct that "waits" for the connection to be made:

mongoose.connection.on("open",function(err) {
    // All app logic or start in here
});

That way you are sure that there is a connection present and the correct objects can be returned and used by the methods. But no connection, and the "Bulk" operations will fail.




回答2:


If you don't care about knowing that the follow has already been created then you can use the $addToSet notation in the initial query:

var myRecord = FollowModel.findOneAndUpdate(
      {facebookId: req.user.facebookId},
      {$setOnInsert: {},
       $addToSet: {players: req.body.idToFollow}},
      {upsert: true, new : true}
    );

I'm not sure what you are doing with that empty $setOnInsert, though.



来源:https://stackoverflow.com/questions/32442886/easier-way-to-update-an-array-with-mongodb

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!