使用mongoose复杂添加到子文档数组

时间:2014-07-10 21:59:11

标签: javascript node.js mongodb mongoose atomic

我的数据模型有帐户,帐户有一些信用交易。我将这些事务设计为子文档:

var TransactionSchema = new Schema({
        amount: Number,
        added: Date
    }),
    AccountSchema = new Schema({
        owner: ObjectId,
        balance: Number,
        transactions: [TransactionSchema]
    });

Transaction添加到Account时,应发生以下情况:

  • transactions推送了新的
  • transactions按日期排序(供以后显示)
  • balance设置为所有transactions
  • 的总和

我现在把它放在Schema.methods - 函数中,在保存之前在JavaScript中执行上述操作。但是,我不确定一次安装多个插件是否安全。

如何在Mongoose中更好地解决使用原子或某种事务更新的问题?在SQL中,我只是做一个事务,但我不能在MongoDB中,那么如何确保transactionsbalance始终正确?

2 个答案:

答案 0 :(得分:4)

您可以使用单个update调用来完成所有这些操作,这些调用结合了所有这三个操作(这是使更新原子组合的唯一方法)。您不会在更新期间对交易求和,而是使用更改量更新balance

var transaction = {
    amount: 500,
    added: new Date()
};

Account.update({owner: owner}, {
        // Adjust the balance by the amount in the transaction.
        $inc: {balance: transaction.amount},
        // Add the transaction to transactions while sorting by added.
        $push: {transactions: {
            $each: [transaction],
            $sort: {added: 1}
        }}
    }, callback);

请注意,这确实使用$push的{​​{3}}修饰符,该修饰符已在2.4中添加并在2.6中更新,因此您需要使用最近的版本。

答案 1 :(得分:2)

几乎与答案相同,只是击败了我,但我确实有更长的解释,所以需要一段时间。

再一次调整也是如此。使用天平上的$inc运算符执行交易是您想要的而不是重新计算的,所以基本上是这段代码:

  bucket.find({
    "account": accId, "owner": owner, "day": day
  }).upsert().updateOne(
    {
      "$inc": { "balance": amount },
      "$push": {
        "transactions": {
          "$each": [{ "amount": amount, "added": date }],
          "$sort": { "added": -1 }
        }
      }
    }
  );

另一个"调整"部分是桶概念。虽然基本上使用数组做这个是一个好主意,使用 $inc 使事务的这一部分成为原子,问题是你不需要很多项目阵列。随着时间的推移,这将会大大增加。

执行此操作的最佳方法是仅在该数组中保留这么多项目,并将这些项目限制为" bucketed"结果。我还在这里增加了一些处理,至少"尝试"保持单一的平衡"指向同步,但实际上您可能希望定期验证,因为生成的多个更新不受事务约束。

但"桶的更新"是原子的。里程可能与实际实施有所不同,但这里是代表示范代码:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/shop');

var ownerSchema = new Schema({
  name: String,
  email: String,
  accounts: [{ type: Schema.Types.ObjectId, ref: "Account" }]
});

var transactionSchema = new Schema({
  amount: Number,
  added: Date
});

var recentBucketSchema = new Schema({
  _id: { type: Schema.Types.ObjectId, ref: "AccountBucket" },
  day: Date
});

var accountSchema = new Schema({
  owner: { type: Schema.Types.ObjectId, ref: "Owner" },
  balance: { type: Number, default: 0 },
  recent: [recentBucketSchema]
});

var accountBucketSchema = new Schema({
  day: Date,
  account: { type: Schema.Types.ObjectId, ref: "Account" },
  owner: { type: Schema.Types.ObjectId, ref: "Owner" },
  balance: { type: Number, default: 0 },
  transactions: [transactionSchema]
});

var Owner = mongoose.model( "Owner", ownerSchema );
var Account = mongoose.model( "Account", accountSchema );
var AccountBucket = mongoose.model( "AccountBucket", accountBucketSchema );

var owner = new Owner({ name: "bill", emal: "bill@test.com" });
var account = new Account({ owner: owner });
owner.accounts.push(account);

var transact = function(accId,owner,amount,date,callback) {

  var day = new Date(
    date.valueOf() - (date.valueOf() % (1000 * 60 * 60 * 24)) );

  var bucket = AccountBucket.collection.initializeOrderedBulkOp();
  var account = Account.collection.initializeOrderedBulkOp();

  bucket.find({
    "account": accId, "owner": owner, "day": day
  }).upsert().updateOne(
    {
      "$inc": { "balance": amount },
      "$push": {
        "transactions": {
          "$each": [{ "amount": amount, "added": date }],
          "$sort": { "added": -1 }
        }
      }
    }
  );

  bucket.execute(function(err,response) {
      if (err) throw err;

      var upObj = {
        "$inc": { "balance": amount }
      };

      if ( response.nUpserted > 0 ) {
        var id = response.getUpsertedIds()[0]._id;
        upObj["$push"] = {
          "recent": {
            "$each": [{ "_id": id, "day": day }],
            "$sort": { "day": -1 },
            "$slice": 30
          }
        };
      }

      console.log( JSON.stringify( upObj, undefined, 4 ) );

      account.find({ "_id": accId }).updateOne(upObj);
      account.execute(function(err,response) {
        callback(err,response);
      });
    }
  );
};

mongoose.connection.on("open",function(err,conn) {

  async.series([

    function(callback) {
      async.each([Owner,Account,AccountBucket],function(model,complete) {
        model.remove(function(err) {
          if (err) throw err;
          complete();
        });
      },function(err) {
        if (err) throw err;
        callback();
      });
    },

    function(callback) {
      async.each([account,owner],function(model,complete) {
        model.save(function(err) {
          if (err) throw err;
          complete();
        });
      },function(err) {
        if (err) throw err;
        callback();
      });
    },

    function(callback) {
      var trandate = new Date();
      transact(account._id,owner._id,10,trandate,function(err,response) {
        if (err) throw err;

        console.log( JSON.stringify( response, undefined, 4 ) );
        callback();
      });
    },

    function(callback) {
      var trandate = new Date();
      trandate = new Date( trandate.valueOf() + ( 1000 * 60 * 60 * 1 ) );
      transact(account._id,owner._id,-5,trandate,function(err,response) {
        if (err) throw err;

        console.log( JSON.stringify( response, undefined, 4 ) );
        callback();
      });
    },

    function(callback) {
      var trandate = new Date();
      trandate = new Date( trandate.valueOf() - ( 1000 * 60 * 60 * 1 ) );
      transact(account._id,owner._id,15,trandate,function(err,response) {
        if (err) throw err;

        console.log( JSON.stringify( response, undefined, 4 ) );
        callback();
      });
    },

    function(callback) {
      var trandate = new Date();
      trandate = new Date( trandate.valueOf() - ( 1000 * 60 * 60 * 24 ) );
      transact(account._id,owner._id,-5,trandate,function(err,response) {
        if (err) throw err;

        console.log( JSON.stringify( response, undefined, 4 ) );
        callback();
      });
    },

    function(callback) {
      var trandate = new Date("2014-07-02");
      transact(account._id,owner._id,10,trandate,function(err,response) {
        if (err) throw err;

        console.log( JSON.stringify( response, undefined, 4 ) );
        callback();
      });
    },

  ],function(err) {

    String.prototype.repeat = function( num ) {
      return new Array( num + 1 ).join( this );
    };

    console.log( "Outputs\n%s\n", "=".repeat(80) );

    async.series([

      function(callback) {
        Account.findById(account._id,function(err,account) {
          if (err) throw err;

          console.log(
            "Raw Account\n%s\n%s\n",
            "=".repeat(80),
            JSON.stringify( account, undefined, 4 )
          );
          callback();
        });
      },

      function(callback) {
        AccountBucket.find({},function(err,buckets) {
          if (err) throw err;

          console.log(
            "Buckets\n%s\n%s\n",
            "=".repeat(80),
            JSON.stringify( buckets, undefined, 4 )
          );
          callback();
        });
      },

      function(callback) {
        Account.findById(account._id)
          .populate("owner recent._id")
          .exec(function(err,account) {
            if (err) throw err;

            var raw = account.toObject();

            raw.transactions = [];
            raw.recent.forEach(function(recent) {
              recent._id.transactions.forEach(function(transaction) {
                raw.transactions.push( transaction );
              });
            });

            delete raw.recent;

            console.log(
              "Merged Pretty\n%s\n%s\n",
              "=".repeat(80),
              JSON.stringify( raw, undefined, 4 )
            );
            callback();
          });
      }

    ],function(err) {
      process.exit();
    });

  });

});

此商家信息使用"批量"更新MongoDB 2.6可用的API功能,但您不必使用它。它只是在这里从更新中转出更有意义的回复。

" bucketing"交易是你要以某种方式拆分它们。这里的基本示例是" day",但可能还有其他更实用的东西。

为了确保在标识符更改时创建新存储桶," upsert"使用MongoDB更新的功能。这通常应该是可以的,因为你以后可以在所有"桶中获得运行平衡,但在这种情况下,我们至少会尝试"尝试"保持"帐户"掌握同步,如果只是为了更多的演示。

当前存储桶的更新完成后,将检查响应以查看" upsert"发生了。在传统或mongoose API .update()下,这只会返回" upserted"的_id。在回调中的第三个参数中记录文档。

" upsert"发生并创建一个新桶,我们也将把它添加到主"帐户"作为最近的桶列表,实际上是最近的30个。因此,这次$push操作会对其他$slice$each操作使用额外的$sort修饰符。

即使只添加一个数组元素,最后两个也需要一起使用。 MongoDB 2.4版本实际上总是要求$slice使用这些修饰符,所以如果你真的不想限制,请将$slice设置为大数,但最好限制长度数组。

在每种情况下,尽管所有示例代码都插入了日期,但日期仍按最新的排序。输出将以这种形式向您显示写操作中实际发生的所有事情,但是总体最终结果的摘要出于阅读目的:

Outputs
========================================================================

Raw Account
========================================================================
{
    "_id": "53bf504ac0716cbc113fbac5",
    "owner": "53bf504ac0716cbc113fbac4",
    "__v": 0,
    "recent": [
        {
            "_id": "53bf504a79b21601f0c00d1d",
            "day": "2014-07-11T00:00:00.000Z"
        },
        {
            "_id": "53bf504a79b21601f0c00d1e",
            "day": "2014-07-10T00:00:00.000Z"
        },
        {
            "_id": "53bf504a79b21601f0c00d1f",
            "day": "2014-07-02T00:00:00.000Z"
        }
    ],
    "balance": 25
}

Buckets
========================================================================
[
    {
        "_id": "53bf504a79b21601f0c00d1d",
        "account": "53bf504ac0716cbc113fbac5",
        "day": "2014-07-11T00:00:00.000Z",
        "owner": "53bf504ac0716cbc113fbac4",
        "transactions": [
            {
                "amount": -5,
                "added": "2014-07-11T03:47:38.170Z"
            },
            {
                "amount": 10,
                "added": "2014-07-11T02:47:38.153Z"
            },
            {
                "amount": 15,
                "added": "2014-07-11T01:47:38.176Z"
            }
        ],
        "balance": 20
    },
    {
        "_id": "53bf504a79b21601f0c00d1e",
        "account": "53bf504ac0716cbc113fbac5",
        "day": "2014-07-10T00:00:00.000Z",
        "owner": "53bf504ac0716cbc113fbac4",
        "transactions": [
            {
                "amount": -5,
                "added": "2014-07-10T02:47:38.182Z"
            }
        ],
        "balance": -5
    },
    {
        "_id": "53bf504a79b21601f0c00d1f",
        "account": "53bf504ac0716cbc113fbac5",
        "day": "2014-07-02T00:00:00.000Z",
        "owner": "53bf504ac0716cbc113fbac4",
        "transactions": [
            {
                "amount": 10,
                "added": "2014-07-02T00:00:00.000Z"
            }
        ],
        "balance": 10
    }
]

Merged Pretty
========================================================================
{
    "_id": "53bf504ac0716cbc113fbac5",
    "owner": {
        "_id": "53bf504ac0716cbc113fbac4",
        "name": "bill",
        "__v": 0,
        "accounts": [
            "53bf504ac0716cbc113fbac5"
        ]
    },
    "__v": 0,
    "balance": 25,
    "transactions": [
        {
            "amount": -5,
            "added": "2014-07-11T03:47:38.170Z"
        },
        {
            "amount": 10,
            "added": "2014-07-11T02:47:38.153Z"
        },
        {
            "amount": 15,
            "added": "2014-07-11T01:47:38.176Z"
        },
        {
            "amount": -5,
            "added": "2014-07-10T02:47:38.182Z"
        },
        {
            "amount": 10,
            "added": "2014-07-02T00:00:00.000Z"
        }
    ]
}