【Firestore】セキュリティールールで楽観的ロックを実装する

※本記事内の「Firestore」は、全てFirestore in native modeを指します

# 楽観的ロックとは

ウィキペディア「[楽観的並行性制御](https://ja.wikipedia.org/wiki/%E6%A5%BD%E8%A6%B3%E7%9A%84%E4%B8%A6%E8%A1%8C%E6%80%A7%E5%88%B6%E5%BE%A1)」より
> 楽観的並行性制御（らっかんてきへいこうせいせいぎょ、optimistic concurrency control）とは、並行性制御(ロック)の手段の種別の一種である。楽観的ロックの概念である。他の処理と競合してはならないトランザクションにおいて、開始時には特に排他処理など行なわず、完了する際に他からの更新がされたか否かを確認し、もし他から更新されてしまっていたら自らの更新処理を破棄し、エラーとする。対照的に悲観的並行性制御がある。

例えばあるドキュメントの詳細情報を表示するページがあって、一部フィールドの値を更新して保存できるボタンがあるとします。

特に何も排他制御を行わない場合、複数のクライアントが同時に詳細情報を表示した後それぞれが更新・保存すると、後から保存した値で上書きされてしまいます。

ユーザーが元の詳細情報を参考にして更新する値を決定したい場合、保存前に他クライアントの更新があると値に不整合が生じます。
これを防ぐ為には詳細情報表示中に他クライアントによる更新が行われていたらエラーにする必要があります。
楽観的ロックという仕組みを導入することでこういった問題を解決することができます。

本記事ではFirestoreのセキュリティルールを利用して楽観的ロックを実装します。

# 準備

予めテスト用ドキュメントを保存しておきます。
![スクリーンショット 2020-12-23 16.10.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/fc97b498-8a5a-25d5-9079-4378348bfa13.png)

versionというフィールドが楽観的ロックに使用されます。
更新時にversionをインクリメントし、セキュリティルールで更新前にその値をチェックします。
他ユーザーが先にインクリメントしているとエラーになります。

# クライアントコード

（FirebaseやFirestoreの初期化等は割愛します）

```javascript
var book;

function getBook() {

    let db = firebase.firestore();

    db.collection("books").doc("test").get().then(function(doc) {
        if (doc.exists) {
            book = doc.data();
            document.getElementById('book-price').value = book.price;
            document.getElementById('book-title').value = book.title;
        } else {
            console.log("No such document!");
        }
    }).catch(function(error) {
        window.alert(`Error submitting record: ${error}`);
    });
}

function updateBook() {

    let db = firebase.firestore();
    
    if (!book) {
        window.alert("get book first!");
        return;
    }

    const title = document.getElementById('book-title').value;

    const price = parseInt(document.getElementById('book-price').value);
    if (isNaN(price)) {
        window.alert('illegal price:' + v);
        return;
    }

    let b = {...book}; // 更新前にコピー（コピーしないとversionが不正に上がってしまう為）
    b.title = title;
    b.price = price;
    b.version++; // 保存前にバージョンをインクリメントする
    
    db.collection("books").doc("test").set(b)
        .then(function() {
            window.alert('saved');
        })
        .catch(function(error) {
            window.alert(`Error submitting record: ${error}`);
        });
}
```

# 実行

getBookを実行すると下記の様にbookドキュメントのtitle、priceフィールドがテキストフィールドに設定されます。
![スクリーンショット 2020-12-23 17.03.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/60ffb2b7-b73c-6367-2058-158559f28fc6.png)

titleとpriceを任意の値に編集します。
![スクリーンショット 2020-12-23 17.06.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/0c4eaa52-5953-2a75-46c2-a2411d718efd.png)

updateBookを実行すると、フィールドに入力したtitle、とpriceの内容でbookドキュメントを更新します。
その際versionをインクリメントしています。

現在はまだ楽観的ロックを実装していない為、複数のウィンドウから同時に操作されてもDocumentは常に上書きされてしまいます。

![スクリーンショット 2020-12-23 17.05.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/c09ca167-01c5-cc90-5d9b-eddbaf88aefe.png)

# セキュリティールール

セキュリティルールを設定して楽観的ロックを組み込みます。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, create: if request.auth.uid != null
      allow create: if request.resource.data.version == 0
      allow update: if request.resource.data.version == resource.data.version+1
    }
  }
}
```

`allow update: if request.resource.data.version == resource.data.version+1` の１行が楽観的ロックチェックです。
`request.resource.data` でこれから更新するデータ、`resource.data` で更新対象のデータを参照出来るので、更新データのversionが既存データのvesion+1の場合にみ更新を許可しています。

また、新規作成はversion=0の時のみ可能としています（これは必須ではないです）。

# 実行2

複数のウィンドウで同じversionのデータを表示します。
![fs-opt-lock-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/64261479-a481-12d6-31ea-81f1c729adfc.png)

片方のクライアントでフィールドを書き換えてupdateBookを実行します。
![fs-optimistic-lock-2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/af80c38c-3a83-08ca-b8a3-74d640ea85ac.png)

これは成功し、データが書き換えられます。versionも正しくインクリメントされています。
![スクリーンショット 2020-12-23 22.16.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/afa2670a-06a1-ce1f-ae6c-8fc363c2f1be.png)

もう片方からもデータを更新してupdateBookを試みます。
![fs-optimistic-lock-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/ed83a7a5-9108-d5d4-49a3-1cb731232dea.png)

想定通りエラーになりました🎉
![スクリーンショット 2020-12-23 22.20.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/fb04b186-5726-1bc2-36f5-c7a4150e0c57.png)

getBookして最新versionを取得し直してから更新すれば成功します。
楽観的ロックが正しく動作していることが確認出来ます。

# 問題点

セキュリティールール違反のエラー内容に「どのルールで失敗したか」を判別する為の情報が含まれてなさそうなので、クライアントに適切なエラーメッセージ（「他のユーザーが値を更新した可能性があります。データを再取得してから実行してください」etc）を表示することが難しそうです(´･ω･`)

ちなみに更新時にcatch出来るエラーの中身は↓の様になっています。
![スクリーンショット 2020-12-23 22.30.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/163e066b-322d-afd5-1f87-1777a73b638b.png)

# まとめ

セキュリティールールを利用して、従来よりも簡単に楽観的ロックを実装出来ました。
セキュリティルールはとても多機能なので、工夫次第でいろいろ出来そうな気がしています(^^)

今後いろいろ試していきたいと思います。