Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8920

Node.js+Sequelizeで楽観的ロックを使って動作を確認する

$
0
0

PONOS Advent Calendar 2020の8日目の記事です。

昨日は@e73ryoさんでした。

はじめに

前回に続いてまたSequelizeの話です。

データの整合性をたもつための排他制御として代表的なものとして悲観的ロックと楽観的ロックというアプローチがあります。
それ自体については沢山記事がありますのでここでは言及致しませんが、基本的に自身のデータしか操作しないようなケースにおいては、操作が競合する可能性が低いため、実現が簡単で不具合の生みにくい楽観的ロックを採用するケースが多いかなと思います。

ということでSequelizeで楽観的ロックを利用する方法をメモ的に書いておきたいと思います。
(といってもめっちゃ簡単な話なんですが)

この記事の対象者

Node.jsとSequelizeの基本的な知識があることを前提とします。
この記事ではこれらの細かい部分については言及しません。

検証環境

  • Node.js 12.19.0
  • Sequelize 6.3.5
  • Sequelize-CLI 6.2.0
  • MySQL 5.7.25

方法

まずは雛形のModelを作成する

今回もSequelize-CLIを使ってModelファイルとマイグレーションファイルを作成します。
ここではuserというテーブルにnameとageフィールドを持たせます。

npx sequelize model:generate --name user --underscored --attributes name:string,age:integer

※ --underscoredを指定しているのはテーブル定義上は名称をスネークケース、コード上はキャメルメースにしたいだけで、本質的には今回の件と関係ありません。
※ 生成されるファイル内容については前回と同様なので省略します。

Modelの設定を変更する

Sequelizeに楽観的ロックの動作を行わせるには、Modelにversion: trueを設定するだけです。

user.jsを編集する

'use strict';const{Model}=require('sequelize');module.exports=(sequelize,DataTypes)=>{classuserextendsModel{/**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */staticassociate(models){// define association here}};user.init({name:DataTypes.STRING,age:DataTypes.INTEGER},{sequelize,modelName:'sample',underscored:true,version:true,});returnuser;};

マイグレーションファイルにversionを追加する

テーブルにversion列が必要なので、定義を追加します。
ちなみにここではversionにデフォルト値を設定していませんが、ORマッパーはINSERT時にも自動的に値を設定するので動作します。

xxxxxxxxxxx-create-user.jsを編集する

'use strict';module.exports={up:async(queryInterface,Sequelize)=>{awaitqueryInterface.createTable('users',{id:{allowNull:false,autoIncrement:true,primaryKey:true,type:Sequelize.INTEGER},name:{type:Sequelize.STRING},age:{type:Sequelize.INTEGER},created_at:{allowNull:false,type:Sequelize.DATE},updated_at:{allowNull:false,type:Sequelize.DATE},version:{allowNull:false,type:Sequelize.INTEGER}});},down:async(queryInterface,Sequelize)=>{awaitqueryInterface.dropTable('users');}};

動作を確認する

うまく動作するのかを検証します。
※ 結論からいうとうまく動作するので、Sequelizeでの楽観的ロックの設定だけ知りたい方は、「以上、終了」という感じです。

マイグレーションを実行してテーブルを準備します。

npx sequelize db:migrate

正常系

まずはインサートしてみる

awaitdb.user.create({name:"sample",age:10});

下記のデータが登録されました。

idnameagecreated_atupdated_atversion
1sample10実行日時実行日時0

次はこれをアップデートしてみます。

constuser=awaitdb.user.findOne({where:{id:1}});user.age+=1;awaituser.save();

下記のようにデータが更新されました。
versionが自動的にインクリメントされており、どうやらうまく動作しているようです。

idnameagecreated_atupdated_atversion
1sample11以前の日時今回の実行日時1

異常系

それでは上記の手順に続いて、本当に楽観ロックが機能しているのかを確認します。

同時にアクセスがあり、トランザクションを開始してデータを取得して更新するケースを想定します。

この時二つのトランザクションA、トランザクションBは同じ状態のデータを取得することになります。
その後、トランザクションAのほうはage+1を行いコミットします。
トランザクションBのほうはage+10を行ってコミットするという想定でコードを書いてみます。

※ トランザクション分離レベルはREPEATABLE READで行っています。

// 同時にトランザクションを開始する。lettransactionA=awaitdb.sequelize.transaction();lettransactionB=awaitdb.sequelize.transaction();try{// まずトランザクションAでデータを取得constuserA=awaitdb.user.findOne({where:{id:1}},{transaction:transactionA});console.log(`A age: ${userA.age} version: ${userA.version}`);// 次にトランザクションBでデータを取得constuserB=awaitdb.user.findOne({where:{id:1}},{transaction:transactionB});console.log(`B age: ${userB.age} version: ${userB.version}`);// トランザクションAがage+1してコミットuserA.age+=1;console.log("A update");awaituserA.save({transaction:transactionA});console.log("A commit");transactionA.commit();transactionA=null;// トランザクションBがage+10してコミットuserB.age+=10;console.log("B update");awaituserB.save({transaction:transactionB});console.log("B commit");transactionB.commit();transactionB=null;}catch(error){console.log(error);if(transactionA){console.log("rollback A");transactionA.rollback();}if(transactionB){console.log("rollback B");transactionB.rollback();}}

これを実行すると下記の順でログが出力されました。

A age: 11 version: 1
B age: 11 version: 1
A update
A commit
B update
OptimisticLockError [SequelizeOptimisticLockError]: Attempting to update a stale model instance: user
(省略)
rollback B

このログから分かる通り、AもB同じデータを参照しており、Aはコミットに成功しましたが、Bはupdate実行時にOptimisticLockError例外が発生し、ロールバックされたようです。

結果のデータベースは下記のようになっています。

idnameagecreated_atupdated_atversion
1sample12以前の日時今回の実行日時2

Aが行った操作であるage+1が反映されていて、versionは1になっています。

まとめ

ということで動作検証も行ったので記事が長くなりましたが、Sequelizeで楽観ロックを導入するのはとても簡単でした。

明日は@kerimekaさんです!


Viewing all articles
Browse latest Browse all 8920

Trending Articles