Javascript 使用mongoose对子文档数组进行复杂添加
我的数据模型有账户,账户有一些信用交易。我已将这些交易设计为子文档:Javascript 使用mongoose对子文档数组进行复杂添加,javascript,node.js,mongodb,mongoose,atomic,Javascript,Node.js,Mongodb,Mongoose,Atomic,我的数据模型有账户,账户有一些信用交易。我已将这些交易设计为子文档: var TransactionSchema = new Schema({ amount: Number, added: Date }), AccountSchema = new Schema({ owner: ObjectId, balance: Number, transactions: [TransactionSchema]
var TransactionSchema = new Schema({
amount: Number,
added: Date
}),
AccountSchema = new Schema({
owner: ObjectId,
balance: Number,
transactions: [TransactionSchema]
});
将交易
添加到帐户
时,应发生以下情况:
已将新事务推送到它事务
按日期排序(供以后显示)交易
设置为所有余额
交易的总和
模式中了。现在,在保存之前,用JavaScript执行上述操作。但是,我不确定同时进行多个插入是否安全
在Mongoose中,如何使用原子更新或某种事务更新更好地解决这个问题?在SQL中,我只想做一个事务,但在MongoDB中我做不到,所以如何确保事务
和余额
总是正确的?您可以通过一个更新
调用来完成所有这些操作(这是使更新组合原子化的唯一方法)。在更新过程中,您不会对交易进行求和,而是使用更改的金额更新余额
:
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
的修饰符,该修饰符在2.4中添加,并在2.6中更新,因此您需要使用最新的版本。您可以通过一个update
调用来完成所有这些操作,该调用组合了所有这三个操作(这是使更新组合成为原子的唯一方法)。在更新过程中,您不会对交易进行求和,而是使用更改的金额更新余额
:
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
的修饰符,该修饰符是在2.4中添加的,在2.6中更新的,因此您需要使用最新的版本。与刚才的答案大致相同,但我有一个较长的解释,因此需要一段时间
所以,再做一次调整,情况基本相同。使用余额上的运算符进行交易是您想要的,而不是重新计算,因此基本上这段代码:
bucket.find({
"account": accId, "owner": owner, "day": day
}).upsert().updateOne(
{
"$inc": { "balance": amount },
"$push": {
"transactions": {
"$each": [{ "amount": amount, "added": date }],
"$sort": { "added": -1 }
}
}
}
);
另一个“调整”部分是桶的概念。虽然基本上对数组执行此操作是一个好主意,并且使用$inc
使事务的这一部分原子化,但问题是您不希望数组中有很多项。随着时间的推移,这将大大增加
最好的方法是在该数组中只保留这么多项,并将这些项限制为“带扣”的结果。我在这里还添加了更多的处理,至少“尝试”保持一个单一的“平衡”点同步,但实际上,您可能希望定期验证,因为生成的多个更新不受事务的约束
但“桶”的更新是原子的。里程数可能因实际实施而异,但以下是基本演示代码:
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”,但可能还有其他更实际的例子
为了确保在标识符更改时创建新的bucket,使用了MongoDB updates的“upsert”功能。这本身应该是正常的,因为您可以稍后在所有“bucket”之间获得一个运行平衡,但在这种情况下,我们至少要“尝试”保持“Account”master的同步,哪怕只是为了进行更多的演示
在当前bucket的更新完成后,将检查响应以查看是否发生了“upsert”。在legacy或mongoose API下,这将在回调的第三个参数中返回“upserted”文档的\u id
如果发生“upsert”并创建了一个新的bucket,我们还将把它添加到主“Account”中,作为最近bucket的列表,实际上是最近30个bucket的列表。因此,这次操作对其他and操作使用了一个额外的修饰符
即使只有一个数组元素要添加,最后两个元素也需要一起使用。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"
}
]
}
我的回答和刚才的答案差不多,但我有一个较长的解释,所以需要一段时间
所以,再做一次调整,情况基本相同。使用余额上的运算符进行交易是您想要的,而不是重新计算,因此基本上这段代码:
bucket.find({
"account": accId, "owner": owner, "day": day
}).upsert().updateOne(
{
"$inc": { "balance": amount },
"$push": {
"transactions": {
"$each": [{ "amount": amount, "added": date }],
"$sort": { "added": -1 }
}
}
}
);
另一个“调整”部分是桶的概念。虽然基本上对数组执行此操作是一个好主意,并且使用$inc
使事务的这一部分原子化,但问题是您不希望数组中有很多项。随着时间的推移,这将大大增加
最好的方法是在该数组中只保留这么多项,并将这些项限制为“带扣”的结果。我在这里还添加了更多的处理,至少“尝试”保持一个单一的“平衡”点同步,但实际上,您可能希望定期验证,因为生成的多个更新不受事务的约束
但“桶”的更新是原子的。里程