原子地更新数组项

时间:2014-09-17 01:41:15

标签: javascript node.js mongodb mongoose mongodb-query

基本问题:

如何检查文档的子数组以查看是否存在条目,如果存在则更新它,如果不存在,则以原子方式添加它?我也想原子地调整计数器(使用$inc),具体取决于第一次操作的结果。

完整解释:

我试图写一个方法来提供原子" upvote"或" downvote"任意mongodb文档的功能。

我希望发生的事情如下:

  • 添加"投票"和" voteScore"给定模式的项目
  • "票"是一组对象,包括投票类型(向上或向下)和投票人的userId
  • " voteScore"是所有投票的总和(升值为+1,downvote为-1)

我试图实施的逻辑是:

  • 当用户投票上或下投票时," voteScore"相应地更新,使用原子$inc运算符,并使用该用户的ID和投票类型向阵列添加新的投票。
  • 当用户更改他/她的投票时,投票评分会以原子方式更新,投票将从数组中移除(如果voteType"none"),或者投票类型已更新到位以反映变化。

我该怎么做呢?我假设我必须使用以太document.update()model.findOneAndUpdate(),因为传统的MongooseArray methods(后跟save())不会是原子的

到目前为止,我有以下代码,但它无效:

var mongoose = require('mongoose');

var voteTypes = ['up', 'down'];
var voteSchema = new mongoose.Schema({
  userId: { type: Schema.Types.ObjectId, ref: 'User',  },
  voteType: { type: String, enum: voteTypes }
});
exports.schema = voteSchema;

exports.plugin = function votesPlugin(schema, options) {
  schema.add({ voteScore: Number });
  schema.add({ votes: [voteSchema] });

  schema.methods.atomicVote = function(args, callback) {
    var item = this;
    var voteType = (args.voteType && args.voteType.toLowerCase()) || 'none';

    var incrementScore, query;

    switch (voteType) {
      case 'none':
        incrementScore = -voteTypeValue(voteRemoved.voteType);
        query = this.update({ $inc: {voteScore: incrementScore}, $pull: { votes: { userId: args.userId } } });
        break;
      case 'up':
      case 'down':
        var newVoteVal = voteTypeValue(args.voteType);
        if (/* Vote does not already exist in array */) {
          incrementScore = newVoteVal;
          query = this.update({$inc: {voteScore: incrementScore}, $addToSet: { userId: args.userId, voteType: args.voteType });
        } else {
          var vote = /* existing vote */;
          if (vote.voteType === args.voteType) return callback(null); // no-op
          var prevVoteVal = voteTypeValue(vote.voteType);
          incrementScore = (-prevVoteVal) + newVoteVal;
          vote.voteType = args.voteType;
          // This likely won't work because document.update() doesn't have the right query param
          // to infer the index of '$'
          query = this.update({$inc: {voteScore: incrementScore}, $set: { 'votes.$': { userId: args.userId, voteType: args.voteType } });
        }
        break;
      default:
        return callback(new Error('Invalid or missing "voteType" argument (possible values are "up", "down", or "none").'));
    }

    query.exec(callback);
  };
};

function voteTypeValue(voteType) {
  switch (voteType) {
    case 'up': return 1;
    case 'down': return -1;
    default: return 0;
  }
}

1 个答案:

答案 0 :(得分:3)

要以原子方式执行此操作,最好将数组分离为"向上/向下"票。通过这种方式,您可以同时从每个字段$push$pull。这主要是因为MongoDB无法在文档中的同一字段路径上的单个更新中执行这些操作。试图这样做会导致错误。

简化的架构表示可能如下所示:

var questionSchema = new Schema({
    "score": Number,
    "upVotes": [{ type: Schema.Types.ObjectId }],
    "downVotes": [{ type: Schema.Types.ObjectId }]
]);

module.exports = mongoose.model( 'Question', questionSchema );

它是一般性的表示,所以不要过于字面意思,但重点是两个阵列。

处理" upvote"你真正需要做的就是确保你没有加入" upvotes"数组在哪里"用户"已存在:

Question.findOneAndUpdate(
    { "_id": question_id, "upVotes": { "$ne": user_id } },
    {
        "$inc": { "score": 1 },
        "$push": { "upVotes": user_id },
        "$pull": { "downVotes": user_id }
    },
    function(err,doc) {

    }
);

反向处理" downvote":

Question.findOneAndUpdate(
    { "_id": question_id, "downVotes": { "$ne": user_id } },
    {
        "$inc": { "score": -1 },
        "$push": { "downVotes": user_id },
        "$pull": { "upVotes": user_id }
    },
    function(err,doc) {

    }
);

这个逻辑扩展就是你只需简单地取消所有选票即可#34;拉动"来自两个阵列。但你确实可以“聪明”#34;关于这一点并保持"状态"您的申请中的信息,以便您了解"如果你正在增加或减少"得分"同样。

所以对我来说,我会这样做,并且还处理"数组"发送响应时的结果只是过滤掉当前用户"对于状态信息,您可以在取消时做出明智的选择,甚至不向服务器发送请求,用户已经在该服务器上投了他们的" upvote"或" downvote"可能是这样。

另请注意,$addToSet不是一个没有查询的选项,因为$inc独立于其他运算符运行。所以你不想"选择"一个无法更新的文档。