JavaのWebアプリケーションからAzureCosmosDBへ接続し値を更新・参照する方法

# 初めに

Azure Cosmos DBを使用してアプリケーションを作成したときに
色々調べたことをまとめて記事にしておこうと思います。

本記事では、Azure Cosmos DBの接続から追加・更新・削除の方法を記載していきます。
Azure Portalについての操作（App ServiceやAzure Cosmos DBのデータベース作成やコンテナの作成方法など）は
記載しません。

CosmosDBを使用したjavaアプリケーションのチュートリアル的なものは公式のドキュメントに
色々あるので、そちらを参照してください。

[チュートリアル: Azure Cosmos DB および NoSQL 用 API を使った Java Web アプリケーションの作成](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/tutorial-java-web-app)

# 前提

Azureで構築する内容になります。

#### Webアプリケーション側（Azure App Service）
一応記載していますが、本記事の内容にはあまり関係ないです。

|      |      |
| ---- | ---- |
| OS | Linux |
| 実行環境 | Java8 |
| Webコンテナ | Tomcat8.5 |

#### データストア側（Azure Cosmos DB）

Azure Portalの画面でデータベースの作成とコンテナーを作成しておきます。
ここも公式のドキュメント参照ください。

[クイックスタート: Azure Cosmos DB for NoSQL のデータを管理するための Java アプリを作成する](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/quickstart-java?tabs=sync#create-a-database-account)の
データベース アカウントの作成の項目を参照ください。

# コードを書く準備

Azure Cosmos DBを使用するためのAPIが必要なのでダウンロードしてビルドパスに含めます。
Mavenを使用する人はチュートリアルに記載されているので、その内容をそのまま使用すれば大丈夫です。

自分が構築したときはGradleを使用したため、Gradleの記述を以下に記載します。

バージョンは現時点（2022/10/18）で最新だったと思います。

```gradle:build.gradle

dependencies {
    // 以下を依存関係に追加
	implementation group: 'com.azure', name: 'azure-cosmos', version: '4.38.0'
}

```

# Azure Cosmos DBへの接続

ここも公式ドキュメントに載っている方法をそのまま使用します。
（と言いたいところですが、その公式ドキュメントの場所を失念したので実際使った内容を載せていきます）

```java

// クライアント生成用ビルダーを生成
CosmosClientBuilder builder = new CosmosClientBuilder();

builder.endpoint("[チュートリアルに載ってある方法で取得]");
builder.key("[チュートリアルに載ってある方法で取得]");

builder.consistencyLevel(ConsistencyLevel.EVENTUAL);
builder.contentResponseOnWriteEnabled(true);

// 接続用クライアントの生成
CosmosClient client = builder.buildClient();

```

接続に必要な情報はendpointとkeyのみで行けると思います。

**builder.consistencyLevel(ConsistencyLevel.EVENTUAL);** と 
**builder.contentResponseOnWriteEnabled(true);** の設定も
公式ドキュメントに載っていた内容だったと思いますが
どういう設定かあいまいな認識です。

contentResponseOnWriteEnabled(true)はCosmosDB側の応答として
ヘッダー情報とステータスコードを返却しますが、実際のデータ情報を
返却するかしないかという感じの設定のようですが、エラー情報とかも
帰ってこなくなる気がするためtrueにしてWebアプリケーション側で
ログから確認できるようにしています。
（の、つもりでしていますが、もしかしたら違うかも）

ちなみに、このクライアントオブジェクトは通信ごとに生成するのではなく、一度生成したものを
再利用することが推奨だそうです。
[データ アクセス オブジェクト (DAO) クラスを追加する](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/tutorial-java-web-app#add-the-data-access-objectdao-classes)

>Azure Cosmos DB サービスを呼び出すには、新しい cosmosClient オブジェクトをインスタンス化する必要があります。 一般に、後続の要求ごとに新しいクライアントを構築するのではなく、cosmosClient オブジェクトを再利用することをお勧めします。

# Azure Cosmos DBへ値の追加・更新・削除

ここからデータの追加・更新・削除について記載していきます。

データの更新などを行う前に、クライアントオブジェクトからデータベース情報を取得し
そこからコンテナー情報を取得する必要があります。

#### データベースの取得

クライアントオブジェクトからデータベース情報を取得します。

```java

CosmosDatabase database = client.getDatabase("[前提で作成したデータベース名]");

```

#### コンテナーの取得

コンテナー（テーブルのようなもの）情報をデータベースオブジェクトから取得します。

```java

CosmosContainer container = database.getContainer("[前提で作成したコンテナー名]");

```

#### データの追加

データの追加では追加したいデータの元になるオブジェクトを作成して
それをそのままAPIに渡すような感じです。

例として、元データを格納するクラスの内容を以下とします。

```java:TestDto.java

private String id;
private String name;

public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

```

このクラスをもとにしてデータを追加します。

```java

TestDto dto = new TestDto();

dto.setId("1");
dto.setName("test");

// 前述で取得したコンテナー（テーブルのようなもの）にデータを追加
container.createItem(dto);

```

idについては追加時に他と被っていると追加時にエラーとなるため、一意なものを設定するような仕組みが必要。

#### データの変更

変更についても追加と大体同じです。

```java

TestDto dto = new TestDto();

dto.setId("1");
dto.setName("replaceTest");

// 先ほど新規追加したものに対して変更する
container.replaceItem(dto, dto.getId(), PartitionKey.NONE, new CosmosItemRequestOptions());

```

第一引数は変更したい内容が入ったデータを入れます。
第二引数は更新対象のIDを入れます。
第三引数はパーティションキーですが、特に使用しないならNONEで。
第四引数は通信時のオプション設定ができるものですが、特にないならnewしたものをそのまま。

IDやパーティションに指定したものに該当するデータがない場合はエラーになります。

#### データの削除

削除についても大体同じです。

```java

TestDto dto = new TestDto();

dto.setId("1");
dto.setName("replaceTest");

// 先ほど変更を加えたものを削除
container.deleteItem(dto, new CosmosItemRequestOptions());

```

第一引数は削除データが入ったもの。
第二引数は通信時のオプションデス。


# Azure Cosmos DBへ値の参照

値の更新については専用のメソッドがありますが、参照についてはSQLを記述してデータを取る方法になるため
そちらについて記載していきます。
（一応、readXXメソッドがいくつかありますが、自分が構築したときはSQLで
　記載された処理を移行することが目的だったので、流用したかった）

SQLの記述の仕方については公式ドキュメントのほうに色々記載しているため
そちらもあわせて参照ください。

[SQL クエリの使用を開始する](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/query/getting-started)
（左の目次から順に参照ください）

#### SQLを使用したデータの参照

横に長くなると、見にくくなるため変数においてます。

```java

// 実行するSQL
String sql = "select * from c";
// 通信時のオプション
CosmosQueryRequestOptions option = new CosmosQueryRequestOptions();
// 取得したデータを変換するクラス
Class<TestDto> dtoClass = TestDto.class;
// クエリを実行して値を取得
CosmosPagedIterable<TestDto> cpi = container.queryItems(sql, option, dtoClass);

```

データを更新する際とあまり変わらないですが、SQLの記法は通常のものを少し違います。
fromに指定している**c**は何者かっていうところですが、これは何でもいいみたいです。

実際のコンテナー名と同じでもいいし、違っててもいいです。
あくまで別名を指定しているだけのようで、問題なく取れます。

また、SELECTの後に項目名を指定する場合、この別名を指定しないと取得時にエラーになります。
（エラーの時の内容を見る限り、返却時のJsonがクラスの構成と変わるためだと思います）

```java
String sql = "select c.id, c.name from c";
```

これはwhere句も同じようです。

```java
String sql = "select * from c where c.id = '1'";
```

#### 取得したデータの取り出し

クエリ実行後の返却されたデータはイテレータ的なものが返ってくるため、
これからイテレータを取得して、ループで内容を取得していきます。

```java

Iterator<TestDto> iterator = cpi.iterator();

while (iterator.hasNext()) {
    TestDto dto = iterator.next();
    // リストに詰めるなり
}

```

# Azure Cosmos DBの終了

クライアントの終了時はクローズ処理を実行します。

```java

client.close();

```

# トランザクションについて

自分のざっくりとした認識でNoSqlはトランザクションとかできないんだろうなぁとか
勝手に思っていましたが、Azure Cosmos DBはトランザクションの実行ができるっぽいことが記載されてました。
[トランザクションとオプティミスティック同時実行制御](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/database-transactions-optimistic-concurrency)

データの更新の項目で記述していた内容では１レコード分しか更新できないので、複数レコードまとめて
更新したりする場合に、途中でエラーがあったらロールバックできないなぁとか思ってたんですが
なんとなくできそうです。
[.NET または Java の SDK を使用した Azure Cosmos DB でのトランザクション バッチ操作](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/transactional-batch?tabs=java)


# 終わりに

Azureでアプリケーションを構築するときに情報収集しようとしたんですが、Azure **Cosmos** DBを
Azure **Commons** DBとずっと間違っていて、「全然情報出てこないじゃん」と思ってました。
（すみません）

公式のドキュメントがたくさんあるので、情報収集に困りませんでしたが、細かいところとかは
自分が見つけられなくて、やったことを記録として残しておこうと思い、今に至ります。

Azure使ってみて、結構いいなと思ってきたので今後も機会があれば触れていきたいと思います。

そういえば、上に記載していないんですが、コンテナー内に生成される**id**について
変換するときのクラスの型がStringじゃないと更新処理の際に正しく反映されないです。

また、nullや空などでも反映されなかったような気がします。