我的数据模型有帐户,帐户有一些信用交易。我将这些事务设计为子文档:
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中,那么如何确保transactions
和balance
始终正确?
答案 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"
}
]
}