ES7の非同期/待機を knex.jsトランザクションと結合しようとしています。
非トランザクションコードを簡単に操作できますが、前述の非同期/待機構造を使用してトランザクションを適切に機能させるのに苦労しています。
このモジュールを使用して非同期/待機をシミュレーションしています
これが私が現在持っているものです:
正常に動作しますが、トランザクションではありません
_// assume `db` is a knex instance
app.post("/user", async((req, res) => {
const data = {
idUser: 1,
name: "FooBar"
}
try {
const result = await(user.insert(db, data));
res.json(result);
} catch (err) {
res.status(500).json(err);
}
}));
_
_insert: async (function(db, data) {
// there's no need for this extra call but I'm including it
// to see example of deeper call stacks if this is answered
const idUser = await(this.insertData(db, data));
return {
idUser: idUser
}
}),
insertData: async(function(db, data) {
// if any of the following 2 fails I should be rolling back
const id = await(this.setId(db, idCustomer, data));
const idCustomer = await(this.setData(db, id, data));
return {
idCustomer: idCustomer
}
}),
// DB Functions (wrapped in Promises)
setId: function(db, data) {
return new Promise(function (resolve, reject) {
db.insert(data)
.into("ids")
.then((result) => resolve(result)
.catch((err) => reject(err));
});
},
setData: function(db, id, data) {
data.id = id;
return new Promise(function (resolve, reject) {
db.insert(data)
.into("customers")
.then((result) => resolve(result)
.catch((err) => reject(err));
});
}
_
_// Start transaction from this call
insert: async (function(db, data) {
const trx = await(knex.transaction());
const idCustomer = await(user.insertData(trx, data));
return {
idCustomer: idCustomer
}
}),
_
await(knex.transaction())
がこのエラーを返すようです:
_[TypeError: container is not a function]
_
Async/awaitはpromiseに基づいているため、「promise互換」オブジェクトを返すには、すべてのknexメソッドをラップする必要があるようです。
ここでは、任意の関数を変換してpromiseで動作するようにして、async/awaitで動作できるようにする方法について説明します。
PromisificationがBlueBirdでどのように機能するかを理解しようとしています
基本的には、これを実行する必要があります。
var transaction = knex.transaction;
knex.transaction = function(callback){ return knex.transaction(callback); }
これは、「async/awaitには、単一のコールバック引数を持つ関数、またはpromiseのいずれかが必要である」ため、knex.transaction
は次のようになります。
function transaction(container, config) {
return client.transaction(container, config);
}
または、新しいasync
関数を作成して、次のように使用できます。
async function transaction() {
return new Promise(function(resolve, reject){
knex.transaction(function(error, result){
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
// Start transaction from this call
insert: async (function(db, data) {
const trx = await(transaction());
const idCustomer = await(person.insertData(trx, authUser, data));
return {
idCustomer: idCustomer
}
})
これも役に立ちます: 約束のあるKnexトランザクション
(また、私はknexのAPIに精通していないので、どのパラメーターがknex.transaction
、上記のものは単なる例です)。
(ロールバックとコミットを使用した)どこでもこれに対する確実な答えを見つけることができなかったので、これが私の解決策です。
まず、knex.transaction
関数を「約束する」必要があります。これにはライブラリがありますが、簡単な例としてこれを行いました:
const promisify = (fn) => new Promise((resolve, reject) => fn(resolve));
この例では、ブログ投稿とコメントを作成し、どちらかでエラーが発生した場合は両方をロールバックします。
const trx = await promisify(db.transaction);
try {
const postId = await trx('blog_posts')
.insert({ title, body })
.returning('id'); // returns an array of ids
const commentId = await trx('comments')
.insert({ post_id: postId[0], message })
.returning('id');
await trx.commit();
} catch (e) {
await trx.rollback();
}
2019年にお越しの方。
Knexをバージョン0.16.5に更新した後。 Knexのtransaction
関数の変更により、sf77の回答が機能しなくなりました。
transaction(container, config) {
const trx = this.client.transaction(container, config);
trx.userParams = this.userParams;
return trx;
}
ソリューション
Sf77のpromisify
関数を保持:
const promisify = (fn) => new Promise((resolve, reject) => fn(resolve));
更新trx
from
const trx = await promisify(db.transaction);
to
const trx = await promisify(db.transaction.bind(db));
私は問題のよりエレガントな解決策を見つけたと思います。
knex Transaction docs を借用して、約束のスタイルと、私にとって有効な非同期/待機のスタイルを対比します。
var Promise = require('bluebird');
// Using trx as a transaction object:
knex.transaction(function(trx) {
var books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
knex.insert({name: 'Old Books'}, 'id')
.into('catalogues')
.transacting(trx)
.then(function(ids) {
return Promise.map(books, function(book) {
book.catalogue_id = ids[0];
// Some validation could take place here.
return knex.insert(book).into('books').transacting(trx);
});
})
.then(trx.commit)
.catch(trx.rollback);
})
.then(function(inserts) {
console.log(inserts.length + ' new books saved.');
})
.catch(function(error) {
// If we get here, that means that neither the 'Old Books' catalogues insert,
// nor any of the books inserts will have taken place.
console.error(error);
});
var Promise = require('bluebird'); // import Promise.map()
// assuming knex.transaction() is being called within an async function
const inserts = await knex.transaction(async function(trx) {
var books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
const ids = await knex.insert({name: 'Old Books'}, 'id')
.into('catalogues')
.transacting(trx);
const inserts = await Promise.map(books, function(book) {
book.catalogue_id = ids[0];
// Some validation could take place here.
return knex.insert(book).into('books').transacting(trx);
});
})
await trx.commit(inserts); // whatever gets passed to trx.commit() is what the knex.transaction() promise resolves to.
})
ドキュメントの状態:
トランザクションハンドラー関数から直接エラーをスローすると、拒否されたpromiseを返すのと同じように、トランザクションが自動的にロールバックされます。
トランザクションコールバック関数は何も返さないか、Promiseを返すことが期待されているようです。コールバックを非同期関数として宣言すると、Promiseが返されます。
このスタイルの利点の1つは、ロールバックを手動で呼び出す必要がないことです。拒否されたPromiseを返すと、自動的にロールバックがトリガーされます。
他の場所で使用したい結果があれば、必ず最後のtrx.commit()呼び出しに渡してください。
私は自分の仕事でこのパターンをテストしましたが、期待どおりに機能します。
Sf77の優れた答えに加えて、TypeScriptでこのパターンを実装して、1つのトランザクションで以下を実行する必要がある新しいユーザーを追加しました。
public async addUser(user: User, hash: string): Promise<User> {
//transform knex transaction such that can be used with async-await
const promisify = (fn: any) => new Promise((resolve, reject) => fn(resolve));
const trx: knex.Transaction = <knex.Transaction> await promisify(db.transaction);
try {
let users: User [] = await trx
.insert({
name: user.name,
email: user.email,
joined: new Date()})
.into(config.DB_TABLE_USER)
.returning("*")
await trx
.insert({
email: user.email,
hash
}).into(config.DB_TABLE_LOGIN)
.returning("email")
await trx.commit();
return Promise.resolve(users[0]);
}
catch(error) {
await trx.rollback;
return Promise.reject("Error adding user: " + error)
}
}