Couchbase Server Java SDK解説：分散トランザクション 〜N1QLクエリ

## N1QLクエリ
Couchbase Server 7.0以降、N1QLクエリはトランザクションラムダ内で使用できるようになりました。N1QLクエリとKey-Value操作とを同じラムダ内で組み合わせて利用することができます。

## トランザクションの開始
Couchbase Serverでは、SDK利用に限らずに見た場合、トランザクションを開始するには、トランザクションライブラリーを使用する方法と、Queryサービスへのリクエストとして、`BEGIN TRANSACTION`を使用する方法の2つがあります。後者の方法は、CLI、REST API、またはWebコンソールUIのクエリワークベンチを使用する場合を想定されています。アプリケーションでは、トランザクションライブラリーを使用します。トランザクションライブラリーは、下記の利点を持ちます。

- エラーと再試行を自動的に処理します。
- これにより、Key-Value操作とN1QLクエリを自由に組み合わせることができます。
- 自動的に`BEGIN TRANSACTION`、`END TRANSACTION`、`COMMIT`および`ROLLBACK`を発行します（ラムダ内でこれらのステートメントを使用すべきではありません）。

## サポートされているN1QL
N1QL DMLステートメントの大部分は、トランザクション内での使用が許可されます。具体的には、`INSERT`、`UPSERT`、`DELETE`、`UPDATE`、`MERGE`、および`SELECT`がサポートされています。

`CREATE INDEX`などのDDLステートメントの利用は許可されていません。

## N1QLの使用
Java SDKのN1QLをすでに使用している場合、トランザクションでの使用は非常に似ています。同じ`QueryResult`を返し、オプションとしてもほとんど同じものが利用できます。

異なる点としては、`cluster.query()`または`scope.query()`ではなく、ラムダ内で`ctx.query()`を使います。

以下は、travel-sampleバケットからいくつかの行を選択する例です。

```JAVA
transactions.run((ctx) -> {
    String st = "SELECT * FROM `travel-sample`.inventory.hotel WHERE country = $1";
    QueryResult qr = ctx.query(st, TransactionQueryOptions.queryOptions()
            .parameters(JsonArray.from("United Kingdom")));
    List<JsonObject> rows = qr.rowsAs(JsonObject.class);
});
```

完全に修飾されたキースペース名（「 `travel-sample`.inventory.hotel」）を指定するのではなく、コンテキスト情報（`Scope`オブジェクトへの参照）を渡すことができます。

```JAVA
Bucket travelSample = cluster.bucket("travel-sample");
Scope inventory = travelSample.scope("inventory");

transactions.run((ctx) -> {
    QueryResult qr = ctx.query(inventory, "SELECT * FROM hotel WHERE country = $1",
            TransactionQueryOptions.queryOptions()
                    .parameters(JsonArray.from("United States")));
    List<JsonObject> rows = qr.rowsAs(JsonObject.class);
});
```

ScopeをUPDATEにを使用することもできます。

```JAVA
String hotelChain = "http://marriot%";
String country = "United States";

transactions.run((ctx) -> {
    QueryResult qr = ctx.query(inventory, "UPDATE hotel SET price = $1 WHERE url LIKE $2 AND country = $3",
            TransactionQueryOptions.queryOptions()
                    .parameters(JsonArray.from(99.99, hotelChain, country)));
    assert(qr.metaData().metrics().get().mutationCount() == 1);
});
```

さらに、SELECTとUPDATEを組み合わせることもできます。ここに示すように、ラムダから通常のJavaメソッドを呼び出すことができ、複雑なロジックを実行できます。ラムダは複数回呼び出される可能性があるため、内部で使われているメソッドも複数回呼び出される可能性があることを意識する必要があります。

```JAVA
transactions.run((ctx) -> {
    // Find all hotels of the chain
    QueryResult qr = ctx.query(inventory, "SELECT reviews FROM hotel WHERE url LIKE $1 AND country = $2",
            TransactionQueryOptions.queryOptions()
                    .parameters(JsonArray.from(hotelChain, country)));

    // This function (not provided here) will use a trained machine learning model to provide a
    // suitable price based on recent customer reviews.
    double updatedPrice = priceFromRecentReviews(qr);

    // Set the price of all hotels in the chain
    ctx.query(inventory, "UPDATE hotel SET price = $1 WHERE url LIKE $2 AND country = $3",
            TransactionQueryOptions.queryOptions()
                    .parameters(JsonArray.from(updatedPrice, hotelChain, country)));
});
```

## Read Your Own Writes（自分自身の書き込みを読む）
N1QLクエリはRead Your Own Writes（自分自身の書き込みを読む）をサポートします。

次の例は、ドキュメントを挿入してから選択する方法を示しています。

```JAVA
transactions.run((ctx) -> {
    ctx.query("INSERT INTO `default` VALUES ('doc', {'hello':'world'})");  

    // Performing a 'Read Your Own Write'
    String st = "SELECT `default`.* FROM `default` WHERE META().id = 'doc'"; 
    QueryResult qr = ctx.query(st);
    assert(qr.metaData().metrics().get().resultCount() == 1);
});
```

挿入されたドキュメントは、その時点では、コミットされていないトランザクションとして、ステージングされます。トランザクションがまだコミットされていないため。他のトランザクション、および他の非トランザクションアクターは、このステージングされた挿入をまだ見ることができません。
ただし、同じトランザクション内では、ステージングされたミューテーション（変更）を読み取るため、SELECTが可能です。

## Key-ValueとN1QLの混合
Key-Value操作とクエリは自由に組み合わせることができ、期待どおりに相互作用します。

この例では、Key-Valueを使用してドキュメントを挿入し、SELECTを使用してそれを読み取ります。

```JAVA
transactions.run((ctx) -> {
    ctx.insert(collection, "doc", JsonObject.create().put("hello", "world")); 

    // Performing a 'Read Your Own Write'
    String st = "SELECT `default`.* FROM `default` WHERE META().id = 'doc'"; 
    QueryResult qr = ctx.query(st);
    assert(qr.metaData().metrics().get().resultCount() == 1);
});
```

「Read　Your　Own Writes」の例と同様に、ここでは挿入はステージングされるだけなので、他のトランザクションや非トランザクションアクターには表示されません。一方、トランザクションラムダ内のクエリーは、同じトランザクション内のKey-Value操作によって挿入されたデータを表示します。

## クエリオプション
トランザクション(`ctc.query`)では、N1QLのクエリオプションを適用するために、`TransactionsQueryOptions`を介して指定します。これは、`QueryOptions`のオプションのサブセットです。

```JAVA
transactions.run((ctx) -> {
    ctx.query("INSERT INTO `default` VALUES ('doc', {'hello':'world'})",
            TransactionQueryOptions.queryOptions().profile(QueryProfile.TIMINGS));
});
```

サポートされているオプションは次のとおりです。


- parameters
- scanConsistency
- flexIndex
- serializer
- clientContextId
- scanWait
- scanCap
- pipelineBatch
- pipelineCap
- readonly
- adhoc
- raw

これらの詳細については、[QueryOptionsのドキュメント](https://docs.couchbase.com/java-sdk/current/howtos/n1ql-queries-with-sdk.html#query-options)を参照してください。

## クエリの同時実行性
Couchbase Server SDKは、リアクティブ（非同期）プログラミングに対応しています。リアクティブAPIを使用すると、操作を複数同時に実行できますが、Queryサービスによって一度に実行されるクエリステートメントは1つだけです。
リアクティブAPIを使用すると、複数の同時クエリステートメントがリクエストされることになりますが、これにより、再試行により内部的にネットワークトラフィックが追加される可能性があり、クエリーの場合は、パフォーマンスが向上する可能性はほとんどありません（Key-Value操作では効果的に働きます）。

### クエリのパフォーマンスアドバイス
ここでは、トランザクションのパフォーマンスを最大化するための情報を整理します。

トランザクションラムダで、クエリステートメントを実行した場合、後続のKey-Value操作はN1QLに変換され、Key-Valueデータサービスではなくクエリサービスによって実行されます。操作は同じように動作し、次の2つの警告を除いて、この実装の詳細はほとんど無視できます。

- クエリサービスは複数のドキュメントを含むステートメント用に最適化されているため、これらの変換されたKey-Value操作はわずかに遅くなる可能性があります。可能な限り最大のパフォーマンスを求めている場合は、可能な時には、ラムダ内の最初のクエリの前にKey-Value操作を配置することで性能を最適化できます。

- リアクティブAPIを使用して同時実行を実現する場合は、変換されたKey-Value操作には、上記と同じ並列処理の制限が適用されることに注意してください。たとえば、クエリサービスによって並列に実行されることはありません。可能な時には、同時Key-Value操作はラムダ内の最初のクエリの前に配置するべきです。

### シングルクエリトランザクション
ここでは、大規模な一括読み込みトランザクションを実行する場合について扱います。

Queryサービスは、必要に応じて、トランザクション内の各ドキュメントのために、メモリを消費します。これは、コミットまたはロールバック時に解放されます。ほとんどのユースケースでは、これは問題になりませんが、非常に多くのドキュメントの一括読み込みなど、ワークロードによっては、サービスに割り当てられたリソースを超える可能性がありえます。これに対する解決策には、ワークロードをより小さなバッチに分割する、クエリサービスに追加のメモリを割り当てる、といったことが考えられます。
あるいは、ここで説明するシングルクエリトランザクションを使用することもできます。

シングルクエリトランザクションには、次の特性があります。

- クエリサービス内のメモリ使用量が大幅に削減される。
- 名前が示すように、1つのクエリで構成され、Key-Value操作は含まれない。

Couchbaseドキュメントの他の場所でへの参照が表示されます。シングルクエリトランザクションは、内部的に`tximplicit`クエリパラメータを`true`に設定します。このパラメーターが`true`の場合、Queryサービスはトランザクションを開始し、ステートメントを実行します。実行が成功すると、Queryサービスはトランザクションをコミットします。それ以外の場合、トランザクションはロールバックされます。

シングルクエリトランザクションは、`transactions.query()`を介して、以下のように開始できます。

```JAVA
String bulkLoadStatement = "INSERT INTO `travel-sample`.inventory.landmark (KEY foo, VALUE bar) SELECT META(doc).id AS foo, doc AS bar FROM `beer-sample` AS doc WHERE type = 'brewery';"; // a bulk-loading N1QL statement

try {
    SingleQueryTransactionResult result = transactions.query(bulkLoadStatement);

    QueryResult queryResult = result.queryResult();
} catch (TransactionCommitAmbiguous e) {
    System.err.println("Transaction possibly committed");
    for (LogDefer err : e.result().log().logs()) {
        System.err.println(err.toString());
    }
} catch (TransactionFailed e) {
    System.err.println("Transaction did not reach commit point");
    for (LogDefer err : e.result().log().logs()) {
        System.err.println(err.toString());
    }
}
```

Scopeを指定して、実行することもできます。

```JAVA
Bucket travelSample = cluster.bucket("travel-sample");
Scope inventory = travelSample.scope("inventory");

transactions.query(inventory, bulkLoadStatement);
```

以下のように`SingleQueryTransactionConfigBuilder`を使って、設定を行うことができます。

```JAVA
transactions.query(bulkLoadStatement, SingleQueryTransactionConfigBuilder.create()
    // Single query transactions will often want to increase the default timeout
    .expirationTime(Duration.ofSeconds(360))
    .build());
```





## 参考情報

https://docs.couchbase.com/server/current/settings/query-settings.html#tximplicit