Spring BootからDynamoDBへのアクセス（Spring Cloud AWS 3.0）

## はじめに

Spring Cloud AWS 3.0を利用してDynamoDBへのアクセス方法をまとめる


## 環境

+ Java 17
+ Spring Boot 3.1.3
+ Spring Cloud AWS 3.0.2
+ DynamoDB local
+ Gradle 8.2.1


## DynamoDB localの設定

+ docker-compose.yamlに下記の定義を行いDynamoDB Localのコンテナを起動
+ dynamodb-adminを追加しているので、`http://localhost:8001/`にアクセスすればGUIでDynaamoDBに接続できる


```docker-compose.yml
version: '3.8'
services:
  dynamodb-local:
    image: amazon/dynamodb-local:latest
    container_name: dynamodb-local
    user: root
    command: -jar DynamoDBLocal.jar -sharedDb -dbPath /data
    volumes:
      - ./data/dynamodb:/data
    ports:
      - "8000:8000"

  dynamo-admin:
    image: aaronshaf/dynamodb-admin:latest
    container_name: dynamo-admin
    environment:
      DYNAMO_ENDPOINT: http://dynamodb-local:8000
    ports:
      - "8001:8001"
    depends_on:
      - dynamodb-local
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110010/a38ad97d-18d7-50a5-dff2-0d064db9cd51.png)


## build.gradle

+ `dependencies`に下記の依存関係を追加

```build.gradle
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.2")
implementation 'io.awspring.cloud:spring-cloud-aws-starter'	
implementation 'io.awspring.cloud:spring-cloud-aws-starter-dynamodb'
```

## application.yml

+ DynamoDBの接続先を設定する(DynamoDB Localのポートを指定)

```
spring:
  cloud:
    aws:
      dynamodb:
        endpoint: http://localhost:4569
        table-prefix: 
```

パラメーターについては下記を参照
https://docs.awspring.io/spring-cloud-aws/docs/3.0.2/reference/html/index.html#configuration


## spring-devtools.properties

+ `spring-boot-devtools`を依存関係に追加していると下記の例外が発生したので抑止するために`src/main/resources/META-INF/spring-devtools.properties`を作成して下記を追加した

```spring-devtools.properties
restart.include.dynamodb=/dynamodb-[-\\w\\d\.]+\.jar
restart.include.awspring=spring-cloud-aws-[-\\w\\d\.]+\.jar
```


### 例外
```
java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:774)
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:755)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:319)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
	at com.example.demo.HttpclientApplication.main(HttpclientApplication.java:35)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50)
Caused by: java.lang.ClassCastException: class com.example.demo.Person cannot be cast to class com.example.demo.Person (com.example.demo.Person is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @6ce9878a; com.example.demo.Person is in unnamed module of loader 'app')
	at software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute.lambda$create$0(ResolvedImmutableAttribute.java:54)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$itemToMap$5(StaticImmutableTableSchema.java:507)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.Collections$UnmodifiableCollection.forEach(Collections.java:1092)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.itemToMap(StaticImmutableTableSchema.java:505)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:67)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:67)
	at software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation.generateRequest(PutItemOperation.java:90)
	at software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation.generateRequest(PutItemOperation.java:45)
	at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CommonOperation.execute(CommonOperation.java:113)
	at software.amazon.awssdk.enhanced.dynamodb.internal.operations.TableOperation.executeOnPrimaryIndex(TableOperation.java:59)
	at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:201)
	at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:209)
	at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:214)
	at io.awspring.cloud.dynamodb.DynamoDbTemplate.save(DynamoDbTemplate.java:59)
	at com.example.demo.HttpclientApplication.run(HttpclientApplication.java:51)
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771)
	... 10 common frames omitted
```


## 使い方

### テーブル定義

+ `@DynamoDbBean`をクラスに付与
+ getterに`@DynamoDbAttribute`を付与
+ パーティションキーのgetterには`@DynamoDbPartitionKey`を付与

```java
import java.util.UUID;

import lombok.Data;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;

@DynamoDbBean
@Data
public class Person {
    private UUID id;
    private String name;
    private String lastName;

    @DynamoDbPartitionKey
    @DynamoDbAttribute("id")
    public UUID getId() {
        return id;
    }
    
    @DynamoDbAttribute("name")
    public String getName() {
      return this.name;
    }
    
    @DynamoDbAttribute("lastName")
    public String getLastName() {
      return this.lastName;
    }
}
```




### テーブル作成

+ `DynamoDbEnhancedClient`を利用する
```java
@Autowired
private DynamoDbEnhancedClient dynamoDbEnhancedClient;
```

**実装例**

```java
DynamoDbTable<Person> table = dynamoDbEnhancedClient.table("person", TableSchema.fromBean(Person.class));
table.createTable();
```



### テーブルへの新規アイテム追加

+ `DynamoDbTemplate`を利用する
```java
@Autowired
private DynamoDbTemplate dynamoDbTemplate;
```

**実装例**

```java
Person person = new Person();
person.setId(UUID.randomUUID());
person.setName("name");
person.setLastName("last");

Person save = dynamoDbTemplate.save(person);
log.info("save =[{}]", save); // save =[Person(id=5dac8d41-b5c9-4ef1-abbe-bacccde71ab0, name=name, lastName=last)] 
```



### テーブルのアイテム更新

+ `DynamoDbTemplate`を利用する
```java
@Autowired
private DynamoDbTemplate dynamoDbTemplate;
```

**実装例**

```java
person.setName("update name");
Person update = dynamoDbTemplate.update(person);
log.info("update =[{}]", update); // update =[Person(id=5dac8d41-b5c9-4ef1-abbe-bacccde71ab0, name=update name, lastName=last)] 
```



### テーブルのアイテム検索

+ `DynamoDbTemplate`を利用する
```java
@Autowired
private DynamoDbTemplate dynamoDbTemplate;
```

**実装例**

```java
Person load = dynamoDbTemplate.load(Key.builder().partitionValue(update.getId().toString()).build(), Person.class);
log.info("load =[{}]", load); // load =[Person(id=5dac8d41-b5c9-4ef1-abbe-bacccde71ab0, name=update name, lastName=last)] 
```


### テーブルのアイテム削除

+ `DynamoDbTemplate`を利用する
```java
@Autowired
private DynamoDbTemplate dynamoDbTemplate;
```

**実装例**

```java
dynamoDbTemplate.delete(update);
```



## 最後に
+ spring-devtoolsとAWSの相性が悪いのか`ClassCastException`が発生してその部分の解決に苦労したが、それ以外は簡単にDynamoDBに接続ができた
+ 検索系のQueryやScan関係のメソッドについてはまだ調べ切れていないのであとでまとめたい・・・



## 参考

https://github.com/awspring/spring-cloud-aws/blob/main/spring-cloud-aws-dynamodb/src/test/java/io/awspring/cloud/dynamodb/DynamoDbTemplateIntegrationTest.java