Spring Boot で Azure Cosmos DBにアクセスする

Spring Bootを利用して、Azure CosmosDB（MongoDBコア）にアクセスしたので、その記録です。

コード書くのが怠かったので、https://spring.pleiades.io/guides/gs/accessing-data-mongodb/ を参考にしました。

まずは、普通にサンプルコードをgit cloneします。

```sh
$ git clone https://github.com/spring-guides/gs-accessing-data-mongodb.git
```

チュートリアル通りにローカルにMongoDBを立てて、

```shell-session
$ sudo apt install mongodb
$ mkdir -p data/db
$ mongod --dbpath=./data/db
```

Gradleで実行します。

```shell-session
$ gradle bootRun

Welcome to Gradle 6.5.1!

Here are the highlights of this release:
 - Experimental file-system watching
 - Improved version ordering
 - New samples

For more details see https://docs.gradle.org/6.5.1/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)

2020-07-02 03:12:00.912  INFO 32643 --- [           main] c.e.a.AccessingDataMongodbApplication    : Starting AccessingDataMongodbApplication on DESKTOP-N9T4CN3 with PID 32643 (XXX/gs-accessing-data-mongodb/complete/build/classes/java/main started by XXX in XXX/gs-accessing-data-mongodb/complete)
2020-07-02 03:12:00.917  INFO 32643 --- [           main] c.e.a.AccessingDataMongodbApplication    : No active profile set, falling back to default profiles: default
2020-07-02 03:12:01.274  INFO 32643 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2020-07-02 03:12:01.315  INFO 32643 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36ms. Found 1 repository interfaces.
2020-07-02 03:12:01.589  INFO 32643 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[localhost:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms', maxWaitQueueSize=500}
2020-07-02 03:12:01.674  INFO 32643 --- [localhost:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:1}] to localhost:27017
2020-07-02 03:12:01.678  INFO 32643 --- [localhost:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=localhost:27017, type=STANDALONE, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 6, 8]}, minWireVersion=0, maxWireVersion=6, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=2881100}
2020-07-02 03:12:01.745  WARN 32643 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.LocalDateTime to class java.time.Instant as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 03:12:01.745  WARN 32643 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.Instant to class java.time.LocalDateTime as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 03:12:01.771  WARN 32643 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.LocalDateTime to class java.time.Instant as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 03:12:01.771  WARN 32643 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.Instant to class java.time.LocalDateTime as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 03:12:02.134  INFO 32643 --- [           main] c.e.a.AccessingDataMongodbApplication    : Started AccessingDataMongodbApplication in 1.552 seconds (JVM running for 1.853)
2020-07-02 03:12:02.157  INFO 32643 --- [           main] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:2}] to localhost:27017
Customers found with findAll():
-------------------------------
Customer[id=5efcd1f2b15c223a7dc2a9ce, firstName='Alice', lastName='Smith']
Customer[id=5efcd1f2b15c223a7dc2a9cf, firstName='Bob', lastName='Smith']

Customer found with findByFirstName('Alice'):
--------------------------------
Customer[id=5efcd1f2b15c223a7dc2a9ce, firstName='Alice', lastName='Smith']
Customers found with findByLastName('Smith'):
--------------------------------
Customer[id=5efcd1f2b15c223a7dc2a9ce, firstName='Alice', lastName='Smith']
Customer[id=5efcd1f2b15c223a7dc2a9cf, firstName='Bob', lastName='Smith']
2020-07-02 03:12:02.270  INFO 32643 --- [extShutdownHook] org.mongodb.driver.connection            : Closed connection [connectionId{localValue:2, serverValue:2}] to localhost:27017 because the pool has been closed.

BUILD SUCCESSFUL in 53s
2 actionable tasks: 2 executed
```

mongoコマンドで中を覗いてみます。

```shell-session
$ mongo
MongoDB shell version v3.6.8
connecting to: mongodb://127.0.0.1:27017
Implicit session: session { "id" : UUID("2648d101-f236-4f56-bc3d-7250d32ef9a2") }
MongoDB server version: 3.6.8
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
        http://docs.mongodb.org/
Questions? Try the support group
        http://groups.google.com/group/mongodb-user
Server has startup warnings: 
2020-07-02T02:54:56.366+0900 I STORAGE  [initandlisten] 
2020-07-02T02:54:56.366+0900 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2020-07-02T02:54:56.366+0900 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] 
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] 
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] ** WARNING: This server is bound to localhost.
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          Remote systems will be unable to connect to this server. 
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          Start the server with --bind_ip <address> to specify which IP 
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          addresses it should serve responses from, or with --bind_ip_all to
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          bind to all interfaces. If this behavior is desired, start the
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] **          server with --bind_ip 127.0.0.1 to disable this warning.
2020-07-02T02:54:57.070+0900 I CONTROL  [initandlisten] 
2020-07-02T02:54:57.071+0900 I CONTROL  [initandlisten] 
2020-07-02T02:54:57.071+0900 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/enabled is 'always'.
2020-07-02T02:54:57.071+0900 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
2020-07-02T02:54:57.071+0900 I CONTROL  [initandlisten] 
> db.customer.find()
{ "_id" : ObjectId("5efcd1f2b15c223a7dc2a9ce"), "firstName" : "Alice", "lastName" : "Smith", "_class" : "com.example.accessingdatamongodb.Customer" }
{ "_id" : ObjectId("5efcd1f2b15c223a7dc2a9cf"), "firstName" : "Bob", "lastName" : "Smith", "_class" : "com.example.accessingdatamongodb.Customer" }
> 
```

入ってますね。

この時点で、チュートリアルでは接続先のMongoDBへのアクセスは行っていません。実装されているデータアクセスに関するものも、以下の２つのみです。

```java:Customer.java
package com.example.accessingdatamongodb;

import org.springframework.data.annotation.Id;

public class Customer {
    @Id
    public String id;
    public String firstName;
    public String lastName;
    public Customer() {}
    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public String toString() {
        return String.format(
                "Customer[id=%s, firstName='%s', lastName='%s']",
                id, firstName, lastName);
    }
}
```

```java:CustomerRepository.java
package com.example.accessingdatamongodb;

import java.util.List;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface CustomerRepository extends MongoRepository<Customer, String> {
    public Customer findByFirstName(String firstName);
    public List<Customer> findByLastName(String lastName);
}
```
高レベルAPIであるRepositoryを使用しているので、実装は非常に簡単です。実際にはSpring Data MongoDBによってCustomerRepojitoryインターフェースを実装するクラスが自動生成されて、saveやfindなどのメソッドも利用できるようになっています。便利ですね。（低レベルAPIであるTemplateMongoを使用すると、もっと複雑なこともできます。）

今回は、MongoDBコアで作成したCosmosDBにアクセスするのが主目的なので、高レベルAPIを利用したこのサンプルを利用します。

Azure側に、CosmosDBを作成します。こちらは https://docs.microsoft.com/ja-jp/azure/cosmos-db/create-mongodb-java#create-a-database-account を参考にしてサクサクとPortalから作成します。コレクションの作成は不要です。

作成されたら、Portalから接続先のURIを確認します。今回はJavaですので、"クイックスタート"の"Java"タブに表示されているプライマリ接続文字列をコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664942/6905ee99-82fc-ede6-9ca0-5e2c52126c6f.png)


サンプルコードに、main/resources/application.propertisを追加し、"spring.data.mongodb.uri"を追加して、先ほどコピーした値を貼り付け、Mongo DB Driverが3.6以降の場合は末尾に"&retrywrites=false"を追加します。Cosmos DBが現時点でRetryable Writesに対応していないためです。

```java:application.properties
spring.data.mongodb.uri=<your_connection_string>&retrywrites=false
````

定義したURLを使用するため、Configurationを作成します。

```java:CustomerRepogitoryConfig.java
package com.example.accessingdatamongodb;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomerRepositoryConfig {
    @Value("${spring.data.mongodb.uri:mongodb://localhost:27017}")
    public String connectionString;
    public @Bean MongoClient mongoClient() {
        return MongoClients.create(this.connectionString);
    }
}
```

Gradleで実行します。

```shell-session
$ gradle bootRun -Pargs=--debug

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)

2020-07-02 04:54:56.451  INFO 3822 --- [           main] c.e.a.AccessingDataMongodbApplication    : Starting AccessingDataMongodbApplication on DESKTOP-N9T4CN3 with PID 3822 (XXX/gs-accessing-data-mongodb/complete/build/classes/java/main started by XXX in XXX/gs-accessing-data-mongodb/complete)
2020-07-02 04:54:56.456  INFO 3822 --- [           main] c.e.a.AccessingDataMongodbApplication    : No active profile set, falling back to default profiles: default
2020-07-02 04:54:57.370  INFO 3822 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2020-07-02 04:54:57.497  INFO 3822 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 118ms. Found 1 repository interfaces.
2020-07-02 04:54:58.011  INFO 3822 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[XXX:YYY], mode=MULTIPLE, requiredClusterType=REPLICA_SET, serverSelectionTimeout='30000 ms', maxWaitQueueSize=500, requiredReplicaSetName='globaldb'}
2020-07-02 04:54:58.011  INFO 3822 --- [           main] org.mongodb.driver.cluster               : Adding discovered server XXX:YYY to client view of cluster
2020-07-02 04:54:58.244  WARN 3822 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.LocalDateTime to class java.time.Instant as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 04:54:58.245  WARN 3822 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.Instant to class java.time.LocalDateTime as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 04:54:58.316  WARN 3822 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.LocalDateTime to class java.time.Instant as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 04:54:58.317  WARN 3822 --- [           main] o.s.data.convert.CustomConversions       : Registering converter from class java.time.Instant to class java.time.LocalDateTime as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation.
2020-07-02 04:54:58.912  INFO 3822 --- [           main] c.e.a.AccessingDataMongodbApplication    : Started AccessingDataMongodbApplication in 3.129 seconds (JVM running for 3.84)
2020-07-02 04:54:58.947  INFO 3822 --- [           main] org.mongodb.driver.cluster               : No server chosen by com.mongodb.client.internal.MongoClientDelegate$1@210386e0 from cluster description ClusterDescription{type=REPLICA_SET, connectionMode=MULTIPLE, serverDescriptions=[ServerDescription{address=XXX:YYY, type=UNKNOWN, state=CONNECTING}]}. Waiting for 30000 ms before timing out
2020-07-02 04:54:59.082  INFO 3822 --- [azure.com:10255] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:1162392059}] to XXX:YYY
2020-07-02 04:54:59.097  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=XXX:YYY, type=REPLICA_SET_PRIMARY, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 6, 0]}, minWireVersion=0, maxWireVersion=6, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=12402600, setName='globaldb', canonicalAddress=XXX:YYY, hosts=[XXX:YYY], passives=[], arbiters=[], primary='XXX:YYY', tagSet=TagSet{[Tag{name='region', value='Japan East'}]}, electionId=null, setVersion=1, lastWriteDate=null, lastUpdateTimeNanos=134393982328436}
2020-07-02 04:54:59.100  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Adding discovered server XXX:YYY to client view of cluster
2020-07-02 04:54:59.103  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Server XXX:YYY is no longer a member of the replica set.  Removing from client view of cluster.
2020-07-02 04:54:59.105  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Canonical address XXX:YYY does not match server address.  Removing XXX:YYY from client view of cluster
2020-07-02 04:54:59.338  INFO 3822 --- [azure.com:10255] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:689958399}] to XXX:YYY
2020-07-02 04:54:59.348  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=XXX:YYY, type=REPLICA_SET_PRIMARY, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 6, 0]}, minWireVersion=0, maxWireVersion=6, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=8755400, setName='globaldb', canonicalAddress=XXX:YYY, hosts=[XXX:YYY], passives=[], arbiters=[], primary='XXX:YYY', tagSet=TagSet{[Tag{name='region', value='Japan East'}]}, electionId=null, setVersion=1, lastWriteDate=null, lastUpdateTimeNanos=134394233832236}
2020-07-02 04:54:59.349  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Setting max set version to 1 from replica set primary XXX:YYY
2020-07-02 04:54:59.349  INFO 3822 --- [azure.com:10255] org.mongodb.driver.cluster               : Discovered replica set primary XXX:YYY
2020-07-02 04:54:59.603  INFO 3822 --- [           main] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:3, serverValue:1139365834}] to XXX:YYY
Customers found with findAll():
-------------------------------
Customer[id=5efcea13cac85c79009dba55, firstName='Alice', lastName='Smith']
Customer[id=5efcea15cac85c79009dba56, firstName='Bob', lastName='Smith']

Customer found with findByFirstName('Alice'):
--------------------------------
Customer[id=5efcea13cac85c79009dba55, firstName='Alice', lastName='Smith']
Customers found with findByLastName('Smith'):
--------------------------------
Customer[id=5efcea13cac85c79009dba55, firstName='Alice', lastName='Smith']
Customer[id=5efcea15cac85c79009dba56, firstName='Bob', lastName='Smith']
2020-07-02 04:55:01.231  INFO 3822 --- [extShutdownHook] org.mongodb.driver.connection            : Closed connection [connectionId{localValue:3, serverValue:1139365834}] to XXX:YYY because the pool has been closed.

BUILD SUCCESSFUL in 8s
3 actionable tasks: 2 executed, 1 up-to-date
```
成功しました。Azureポータルのデータエクスプローラーでcustomerコレクションが追加されていることを確認しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664942/7a9cfd42-624a-ff61-6948-7701c5825c8d.png)

データが作成されていることが確認できますね。

Cosmos DBは最近Free Tierが提供されたので、手軽に機能を試すことができます。
是非、触ってみてください。