web-dev-qa-db-ja.com

ES7で動作するKnex.jsトランザクションを非同期/待機する

ES7の非同期/待機を knex.jsトランザクションと結合しようとしています。

非トランザクションコードを簡単に操作できますが、前述の非同期/待機構造を使用してトランザクションを適切に機能させるのに苦労しています。

このモジュールを使用して非同期/待機をシミュレーションしています

これが私が現在持っているものです:

非トランザクションバージョン:

正常に動作しますが、トランザクションではありません

app.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);
  }
}));
_

user.js

_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));
  });
}
_

それをトランザクションにしようとする

user.js

_// 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]_

17
Nik Kyriakides

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、上記のものは単なる例です)。

10
Lance Pollard

(ロールバックとコミットを使用した)どこでもこれに対する確実な答えを見つけることができなかったので、これが私の解決策です。

まず、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();
}
27
sf77

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));
4
Peter Vu

私は問題のよりエレガントな解決策を見つけたと思います。

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()呼び出しに渡してください。

私は自分の仕事でこのパターンをテストしましたが、期待どおりに機能します。

4
nigel.smk

Sf77の優れた答えに加えて、TypeScriptでこのパターンを実装して、1つのトランザクションで以下を実行する必要がある新しいユーザーを追加しました。

  1. uSERテーブルにユーザーレコードを作成する
  2. lOGINテーブルにログインレコードを作成する
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) 
        }
}
3
gomisha