LoginSignup
14
15

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-05-16

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

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

②更新したいホテルの「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つ用意して、同じホテルの更新します。

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

20161007190349.png

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

20161007190402.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です。

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

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

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

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

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

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

    }
}

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

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

Entityクラス

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

Repositoryクラス

@Repository
public interface HotelRepository extends JpaRepository<Hotel, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    Hotel findOne(Long id);
}

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

Serviceクラス(訂正後)

@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=?

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

14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15