Couchbase Server Java SDK解説：分散トランザクション〜キーバリュー操作API（解説と利用例）

## はじめに

Couchbase　SDKを使った、Javaプログラミングにおけるトランザクションの扱いを具体的なAPIコールに即して解説します。

本記事の関連情報として以下をご参照ください。

[Couchbase Java SDK解説：分散トランザクションの内部メカニズム](https://qiita.com/yoshiyuki_kono/items/2d428b16b224b8c01239)

[Couchbase Java SDK解説：分散トランザクション〜エラー処理を理解する](https://qiita.com/yoshiyuki_kono/items/d7eb2b9553d858c7cb93)

[Couchbase Java SDK解説：分散トランザクションプログラミング入門](https://qiita.com/yoshiyuki_kono/items/4efb2f1f5a03051e28c5)


## ドキュメントへのK/Vアクセス

#### ドキュメントの取得
ドキュメントを取得するには、`get`と`getOptional`の2つのメソッドがあります。

まず、`getOptional`について見てみます。このメソッドでは、指定されたIDにマッチするドキュメントが存在しない場合、`Optional.empty() `が返されます。

```java
transactions.run((ctx) -> {
    String docId = "a-doc";

    Optional<TransactionGetResult> docOpt = ctx.getOptional(collection, docId);
    if(doc.isPresent()) {
        // ドキュメントに対する処理
    }
});
```

まず、`get`について見てみます。

```java
transactions.run((ctx) -> {
    String docId = "a-doc";

    TransactionGetResult doc = ctx.get(collection, docId);
});
```

`get`では、開発者はドキュメントが存在するかどうかを確認する必要はありません。指定されたIDにマッチするドキュメントが存在しない場合、`TransactionFailed`例外とともに、処理が失敗します。


#### ドキュメントの挿入

以下は、ドキュメント挿入のコードスニペットです（トランザクションプログラミングに固有のことではありませんが、指定したIDのドキュメントが既に存在している場合は、処理は失敗します）。

非同期API：

```java
transactions.reactive().run((ctx) -> {
    return ctx.insert(collection.reactive(), "docId", JsonObject.create()).then();
}).block();
```


同期API：

```java
transaction.run（（ctx）-> { 
    String docId = "docId"; 

    ctx.insert（collection、docId、JsonObject.create（））; 

}）;
```

トランザクション中で挿入されたドキュメントは、そのトランザクション内で取得することが可能です（これは、想定通りの挙動であるはずです）。例えば以下のコードは成功します。

```java
transactions.run((ctx) -> {
    String docId = "docId";

    ctx.insert(collection, docId, JsonObject.create());

    Optional<TransactionGetResult> doc = ctx.getOptional(collection, docId);

    assert (doc.isPresent());
});
```

#### ドキュメントの置換

トランザクション内で、ドキュメントを置換するには、最初に`ctx.get()`をコールする必要があります。これは、そのドキュメントが別のトランザクションに関与しないようにするために必要となります。（そのドキュメントが別のトランザクションに関与している場合、トランザクションは適切にこれを処理します。通常、これまでに行われたことをロールバックし、ラムダを再試行します。）

同期API：

```java
transactions.run((ctx) -> {
    TransactionGetResult anotherDoc = ctx.get(collection, "anotherDoc");
    JsonObject content = anotherDoc.contentAs(JsonObject.class);
    content.put("transactions", "are awesome");
    ctx.replace(anotherDoc, content);
});
```

非同期API：

```java
transactions.reactive().run((ctx) -> {
    return ctx.get(collection.reactive(), "anotherDoc").flatMap(doc -> {
        JsonObject content = doc.contentAs(JsonObject.class);
        content.put("transactions", "are awesome");
        return ctx.replace(doc, content);
    }).then(ctx.commit());
});
```

#### ドキュメントの削除

置換と同様、ドキュメントを削除するには、最初に`ctx.get()`をコールする必要があります。

非同期API：

```java
transactions.reactive().run((ctx) -> {
    return ctx.get(collection.reactive(), "anotherDoc").flatMap(doc -> ctx.remove(doc));
});
```

同期API：

```java
transactions.run((ctx) -> {
    TransactionGetResult anotherDoc = ctx.get(collection, "anotherDoc");
    ctx.remove(anotherDoc);
});
```

## 包括的な利用例

ここでは、マルチプレイヤーオンラインゲームをシミュレートした例を紹介します。この例では、プレイヤーが、モンスターにダメージを与えます。プレーヤーとモンスターの情報は個々のドキュメントとして管理されており、トランザクション内で、ヒットポイント、経験値などの情報の更新が行われます。
（実際には、このようなゲームの挙動を実現する際に、トランザクションのためのパフォーマンスコストを支払う代わりに、リスクが低く、影響が限定されているという判断のもと、非トランザクションプログラミングを選択することも十分に考えられます。）


```java
public void playerHitsMonster(int damage, String playerId, String monsterId) {
    Transactions transactions = getTransactions();

    try {
        transactions.run((ctx) -> {
            TransactionGetResult monsterDoc = ctx.get(collection, monsterId);
            TransactionGetResult playerDoc = ctx.get(collection, playerId);

            int monsterHitpoints = monsterDoc.contentAs(JsonObject.class).getInt("hitpoints");
            int monsterNewHitpoints = monsterHitpoints - damage;

            if (monsterNewHitpoints <= 0) {
                // ヒットポイントが0以下になったため、モンスターが殺されます。
                // 以下では、（例示としての簡便のため）ドキュメントを削除していますが、
                // 実際的には、フラグの値を変更することが考えられます。
                ctx.remove(monsterDoc);

                // プレイヤーはモンスターを殺すことによって経験値を獲得します
                int experienceForKillingMonster = monsterDoc.contentAs(JsonObject.class)
                        .getInt("experienceWhenKilled");
                int playerExperience = playerDoc.contentAs(JsonObject.class).getInt("experience");
                int playerNewExperience = playerExperience + experienceForKillingMonster;
                int playerNewLevel = calculateLevelForExperience(playerNewExperience);

                JsonObject playerContent = playerDoc.contentAs(JsonObject.class);

                playerContent.put("experience", playerNewExperience);
                playerContent.put("level", playerNewLevel);

                ctx.replace(playerDoc, playerContent);
            } else {
                // モンスターはダメージを受けていますが、まだ生きています
                JsonObject monsterContent = monsterDoc.contentAs(JsonObject.class);
                monsterContent.put("hitpoints", monsterNewHitpoints);

                ctx.replace(monsterDoc, monsterContent);
            }
        });
    } catch (TransactionFailed e) {
        // 何らかの理由でトランザクションが失敗しました。
        // トランザクション中の変更は全て、ロールバックされます。
    }
}
```