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` フィールドの追加

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

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

これで更新時に、

```
Hibernate: update table set colmun=? where version=?
```
といった感じにversionで条件指定されるようになります。

　

## 更新用のServiceクラス 

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

```java
@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つ用意して、同じホテルの更新します。

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

![20161007190349.png](https://qiita-image-store.s3.amazonaws.com/0/109149/f878bd34-a31c-2795-a657-eca9bfda0f02.png)


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

![20161007190402.png](https://qiita-image-store.s3.amazonaws.com/0/109149/8b059695-be31-8412-6b55-bbef77d47e49.png)

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

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

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

```
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'`は、下記で作成されるmessageです。

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

問題なく動いているようには見えるものの、あまりよい実装方法ではないと思い、色々と調べてみると問題があったので訂正内容を記載します。

# 訂正(今回の制御の方法)

Serviceクラスに、`EntityManager`を利用して楽観的ロックを行っておりましたが、

`JpaRepository`を用いる場合の方法が異なっておりました。


**Serviceクラス（訂正前）**

```java
@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);

    }
}
```

JpaRepositoryを用いる場合は、Entityクラスに`@Version`フィールドを設置し

Repositoryクラスで、下記のようにアノテーションを用いて楽観的ロックを実装します。

**Entityクラス**

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

**Repositoryクラス**

```java
@Repository
public interface HotelRepository extends JpaRepository<Hotel, Long> {
	
	@Lock(LockModeType.OPTIMISTIC)
	Hotel findOne(Long id);
}
```

よってServiceクラスではEntityManagerは不要です。

**Serviceクラス（訂正後）**

```java
@Service
@Transactional
public class HotelUpdateService {

    @Autowired
    private HotelRepository hotelRepository;

    /**
     * 
     * @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()));


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

    }
}
```

Unitテストをした際に `@MonckBean`を定義していたServiceクラスで

```
NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' is defined
```

のようなエラーが発生して、EntityManagerの利用に問題が発覚し、今回の訂正にいたりました。

どうやら、`@PersistenceContext`を複数定義していたことに問題がありそうでした。

これでUpdateのリクエストを投げると、下記のようにversionで条件指定して、インクリメントするようなクエリが発行されます。

```
Hibernate: update hotel set ..., version=? where id=? and version=?
```

それぞれの仕組みについての理解があまく、だいぶはまってしまいました。