Sequelizeを使用してトランザクション処理を行う


初めに

今回は、Sequelizeのトランザクションを使用して、お金の送金を行う処理を実装します。
所持金1000円のたかしさんと所持金500円の花子さんがいると仮定します。

上図のように、たかしさんが花子さんに500円を送金したとします。
結果は、下図のように、たかしさんの所持金が500円、花子さんの所持金が1000円になります。

処理の流れとしては
①たかしさんの所持金を1000円から500円に更新
②花子さんの所持金を500円から1000円に更新
の順番で実装したいと思います。

この処理の際に、①の処理は成功したが②の処理は失敗した場合、たかしさんの所持金だけ減っていてはいけません。
トランザクションを使用して、どちらか一方でも処理が失敗した場合にはデーターの保存を行わず、どちらも成功した場合のみデーターの保存を行います。

既にテーブルを作成し、モデルの定義を実装済みです。

■ テーブルの構造

■ モデルの定義

users.js
'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class User extends Model {
    static transact() {
      //ここに処理を記述
    }
  };
  User.init({
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    name: DataTypes.STRING,
    money: DataTypes.INTEGER,
    createdAt: DataTypes.DATE,
    updatedAt: DataTypes.DATE
  }, {
    sequelize,
    modelName: 'Users',
  });
  return User;
};

今回は、Userクラスのtransactというクラスメソッドを使用してトランザクション処理を行います。

環境

■ 言語
Javascript(Node.js)
■ フレームワーク
Express
■ RDBMS
MySQL
■ 使用しているパッケージの詳細

package.json
  "dependencies": {
    "express": "~4.16.1",
    "mysql2": "^2.3.0",
    "sequelize": "^6.6.5"
  }

Sequelizeのトランザクションの種類

Sequelizeのトランザクションには2種類の方法があります。
Unmanaged transactions: コミットとロールバックを手動で記載して行う方法です。
Managed transactions: 自動的にコミットとロールバックを行う方法です。

今回は、2つ目の自動的にコミットとロールバックを行う方法で実装していきます。

実装

データの更新にはupdateメソッドを使用します。
本来は1つのupdateメソッドで処理は完結しますが、今回はトランザクション処理を使用するため、意図的に2回メソッドを使用します。
結論として下記のコードで実装可能です。
1つ1つの記述がなぜ必要であるか次の章で記述します。

class User extends Model {
    static transact() {
      sequelize.transaction(async function(tx){
        //たかしさんの所持金を500円に変更
        await User.update({ money: 500 },{ where: {id: 1}, transaction: tx })
        //花子さんの所持金を1000円に変更
        await User.update({ money: 1000 },{ where: {id: 2}, transaction: tx })
         //戻り値を設定(必要に応じて戻り値は変更する必要がある)
      return User
      });
    };
 };

transactionメソッド

MySQLのデフォルトでは、自動コミットモードが有効になった状態で動作します。
そのため、自動コミットモードを無効にしてトランザクションを開始する必要があります。
それには以下の記述を行います。

static transact() {
  sequelize.transaction()
}

transactionメソッドを使用することによって、トランザクションを開始することができます。

async/await

async/awaitを使用しないで処理をした場合は、以下のように、所持金の更新より先にコミットが実行されてエラーが出力されます。

Executing (f502d728-f4bd-436a-b120-5da3ad19f002): START TRANSACTION;
Executing (f502d728-f4bd-436a-b120-5da3ad19f002): COMMIT;
Error: commit has been called on this transaction(f502d728-f4bd-436a-b120-5da3ad19f002), you can no longer use it. (The rejected query is attached as the 'sql' property of this error)

トランザクション処理の間にデーターの更新を行いたいので、awaitを使用して同期通信で処理を行っています。

transaction: tx

updateメソッドの第2引数に「transaction: tx」という記述をしています。
これは下記コードのトランザクションと一緒に処理をするという意味合いの記述になります。

sequelize.transaction(async function(tx){}

「transaction: tx」の記述がない場合に、エラーが投げられた際は以下のような結果になり、エラーが出力されてもデータが保存されてしまいます。

Executing (5cdcd238-f7ec-40c7-bb26-3ff157025a3c): START TRANSACTION;
Executing (default): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (default): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (5cdcd238-f7ec-40c7-bb26-3ff157025a3c): ROLLBACK;

これでは、送金時にどちらか一方が失敗した場合にもコミットされてしまうので、「transaction: tx」という記述が必要です。

トランザクションが正常に動作した場合

■ エラーが発生しなかった場合

下記の結果で、送金に関するデータがコミットされていることを確認できました。

Executing (30cff9ad-07b5-4fbe-b66c-0aa58df6609b): START TRANSACTION;
Executing (30cff9ad-07b5-4fbe-b66c-0aa58df6609b): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (30cff9ad-07b5-4fbe-b66c-0aa58df6609b): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (30cff9ad-07b5-4fbe-b66c-0aa58df6609b): COMMIT;

テーブル


テーブルの「money」カラムの値が変わっていることが確認できました。

■ エラーが発生した場合
エラーが起きた場合の処理を確認するために、throw文を使用してtransactionメソッド内にエラーを発生させます。

class User extends Model {
    static transact() {
      sequelize.transaction(async function(tx){
        //たかしさんの所持金を500円に変更
        await User.update({ money: 500 },{ where: {id: 1}, transaction: tx })
        //花子さんの所持金を1000円に変更
        await User.update({ money: 1000 },{ where: {id: 2}, transaction: tx })
         //エラーを発生させる
      throw new Error
      });
    };
 };

以下のように、UPDATE文がコミットされずに正常にロールバックされていることが確認できます。

Executing (8d5c8ed2-fea7-4b93-8d9a-08200dfcd527): START TRANSACTION;
Executing (8d5c8ed2-fea7-4b93-8d9a-08200dfcd527): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (8d5c8ed2-fea7-4b93-8d9a-08200dfcd527): UPDATE `Users` SET `money`=?,`updatedAt`=? WHERE `id` = ?
Executing (8d5c8ed2-fea7-4b93-8d9a-08200dfcd527): ROLLBACK;

テーブルを確認すると、所持金が変わっていないことが確認できます。

以上になります。
ありがとうございました。

参考文献

Sequeizeのトランザクションに関して
https://sequelize.org/master/manual/transactions.html
MySQLのトランザクションに関して
https://dev.mysql.com/doc/refman/5.6/ja/commit.html