読者です 読者をやめる 読者になる 読者になる

修行@ホーチミン

ホーチミン長期出張の日記です

Webアプリ DB更新時の排他制御

このアプリでの更新の流れ

①ホテル一覧が表示される。

②更新したいホテルの「UPDATEボタン」をクリック

③更新用のモーダルダイアログの表示

④更新ボタンでAPIへDB更新のリクエスト

⑤成功or失敗をアラートで表示

 

やりたいこと

ある人物Aさんが①-④の操作を行う間に、

Bさんが同じホテルの更新を既に更新していた場合、

Aさんのホテル更新は失敗し、⑤でエラーを表示する。

 

今回の制御の方法

versionカラムの追加

MySQLの該当テーブルにversionカラムを追加します。

※他カラム省略

+-------------------+---------------+------+-----+---------+----------------+
| Field             | Type          | Null | Key | Default | Extra          |
+-------------------+---------------+------+-----+---------+----------------+
| version           | int(10)       | NO   |     | 0       |                |
+-------------------+---------------+------+-----+---------+----------------+
 

 

さらに、ホテルリスト表示にもちいるResponseにversion情報を追加しておきます。

Responseの一部

  "body": {
    "items": [
      {
        "id": 369,
        "name": "The Plaza Hotel",
        "address": "Fifth Avenue at Central Park South 10019",
        "countryCode": "US",
        "cityCode": "NYC",
        "grade": 5,
        "facility": "託児サービス |エレベーター |監視つきチャイルドケア / アクティビティ サービス |美容室 |ATM / 銀行 |コンシェルジュ サービス |ギフトショップまたはニューススタンド |ショップ (敷地内) |客室総数 - 282|フロアの数 - 20|朝食あり (有料) |ランドリー設備 |図書室 |セーフティボックス (フロントデスク) |スパサービス (敷地内) |複数の言語を話すスタッフ |24 時間対応フロントデスク |ビジネスセンター |エクスプレス チェックアウト |ドライクリーニング / ランドリーサービス |リムジンまたはタウンカーサービスあり |フルサービス スパ |スパ トリートメントルーム |スチームサウナ |サウナ |ウェディング サービス |エクスプレス チェックイン |ツアー / チケット案内 |WiFi (有料) |会議室 1 室 |バレーパーキング (有料) |手荷物保管サービス |バー / ラウンジの数 - 3|レストランの数 - 5|フィットネス設備 |新聞 (ロビー、無料) |託児サービス (有料)",
        "imagePath": "http://media.expedia.com/hotels/1000000/30000/28100/28044/28044_210_b.jpg",
        "version": 0
      },

 

このversionを更新時の比較に用いるためです。

 

Entity に @Version フィールドの追加

    @Column(name = "version")
    @Version
    private int version;

 

javax.persistence.Version@Versionで管理します。

これで更新時に、

Hibernate: update table set colmun=? where version=?

といった感じにversionで条件指定されるようになります。

 

更新用のServiceクラス

もっとよい方法があるのではと思いつつ、調べてもこれといった情報を見つけれず現状こうなりました。

@Service
@Transactional
public class HotelUpdateService {

    @Autowired
    private HotelRepository hotelRepository;
    
    @PersistenceContext
    private EntityManager entityManager;

    /**
     * 
     * @return
     */
    public HotelUpdateResponseBody updateHotel(HotelInfo hotelInfo) {
               
        Hotel target = hotelRepository.findOne(hotelInfo.getId());
        
    //ここで「このアプリでの更新の流れ」①で表示したときのversionと、現在のversionを比較
        if(target.getVersion() != hotelInfo.getVersion()){
            throw new ObjectOptimisticLockingFailureException(Hotel.class, hotelInfo.getId());
        }

        
        //省略してますが、更新内容をsetする処理       
        target.setUpdateDatetime(new Timestamp(System.currentTimeMillis()));
              

        //楽観的ロック
        entityManager.lock(target, LockModeType.OPTIMISTIC); 


     //更新処理
        Hotel updatedHotel = hotelRepository.save(target);

    }
}

 

updateHotel(HotelInfo hotelInfo)メソッドの引数であるHotelInfoには、

ホテルリスト表示にレスポンスで渡したversion情報が含まれ、そのversionを処理時に現在のversionと比較してます。

上記ソースコード内//楽観的ロックでは、トランザクションレベルでの制御のようです。

正常に更新が完了された場合は、versionがインクリメントされます。

この制御だけでは、今回の要件を満たすことはできなさそうでした。

 

画面側

ここまで出準備は整っているので、後は更新リクエスト時

hiddenタグのvalueにホテルリスト表示時のidを持たせればOKです。

 

確認

ブラウザの画面を2つ用意して、同じホテルの更新します。

下記は、更新用モーダルダイアログ表示をしている画像です。

f:id:bebe0909:20161007190349p:plain

この2つのブラウザのうち、左側を先に更新してそのあと右を更新してみます。

f:id:bebe0909:20161007190402p:plain

左側は更新に成功し、右側は更新に失敗することが確認できたので

「やりたいこと」ができていそうです。

 

念の為、ログを確認してみます。

ApiHttpException: {'status': 400, 'error': {'invalidParameters': None, 'message': 'Already updated error', 'code': 'HTLERR100', 'type': 'VALIDATION', 'detailMessage': 'Object of class [com.denatravel.api.hotel.repository.entity.Hotel] with identifier [396]: optimistic locking failed'}}

 

Serviceクラスで投げたExceptionが出力されていました。

※ここで表示されるログの情報はExceptionHandlerクラスで設定してます。

detailMessageは、

 throw new ObjectOptimisticLockingFailureException(Hotel.class, hotelInfo.getId());

 で作成されるmessageです。

 

 

現状問題なく動いているようには見えるものの、あまりよい実装方法ではない気がしてなりませんが、

詳しく書かれたドキュメントを見つけることができなかったのでこんな感じになっています。

これで起こりうるバグは何かあるのでしょうか..。

※方法に問題があったので、また更新します。