[英]Complex addition to subdocument array with mongoose
我的數據模型具有“帳戶”,並且一個帳戶具有一些信用交易。 我將這些事務設計為子文檔:
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
始終正確?
您可以通過將所有這三個操作組合在一起的單個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);
請注意,這確實利用了$sort
$push
的$sort
修飾符,該修飾符是在2.4中添加並在2.6中更新的,因此您需要使用最新的構建。
答案幾乎與打敗我的目的相同,但我的解釋時間較長,因此需要一段時間。
因此,再進行一次調整,幾乎是同一件事。 您想要使用余額上的$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
的使用使事務的這一部分變得原子化,但問題是您不希望數組中有很多項目。 隨着時間的流逝,這將大大增加。
最好的方法是僅在該數組中保留許多項目,並將其限制為“存儲桶”結果。 我還在這里至少增加了一些處理,以便至少“嘗試”使單個“平衡”點保持同步,但是實際上,您可能希望定期進行驗證,因為由此產生的多個更新不受事務約束。
但是“存儲桶”的更新是原子的。 里程可能會因實際實施而異,但這是基本的演示代碼:
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功能,但您不必使用它。 只是在這里轉儲更新中更有意義的響應。
“存儲”交易的一般情況是,您將以某種方式將其拆分。 這里的基本示例是“日”,但可能更實用。
為了確保您在標識符更改時創建新存儲桶,將使用MongoDB更新的“ upsert”功能。 通常,這本身就可以,因為您以后可以在所有“存儲桶”中獲得運行中的余額,但是在這種情況下,我們至少要“嘗試”保持“帳戶”主數據保持同步,如果再保持一點時間的話示范。
當前存儲桶上的更新完成后,將檢查響應以查看是否發生了“更新”。 在舊版或貓鼬API .update()
這將僅在回調的第三個參數中返回“上載”文檔的_id
。
在發生“ upsert”並創建新存儲桶的情況下,我們還將將其添加到主“ Account”中,作為最近存儲桶的列表,實際上是最近存儲桶的列表。 因此,這次$push
操作對其他$each
和$sort
操作使用了一個額外的$slice
修飾符。
即使僅添加一個數組元素,最后兩個也需要一起使用。 實際上,MongoDB 2.4版本始終需要使用這些修飾符使用$slice
,因此,如果您真的不想限制,請將$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"
}
]
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.