やはり俺の技術チョイスはまちがっている (Spring Data DynamoDB 編)

# はじめに
（長文です。結論だけ知りたい方は最後だけ読んでください）

このページにたどり着いた方はきっと、 Java で Amazon DynamoDB を使うことになって、普段使うフレームワークがが Spring なので、Spring のエコシステムに乗っかる形で DynamoDB が使えたら…とか考えている人だと思います。

私もそんな人間の一人でした。
そこで、同士達が、同じ苦労をしないように、ここに記録を残します。

# 諸君、私は Spring が好きだ
好きなんですよ。Spring。

なんで好きかって言われるとよくわからないんですが、Springって名前でフレームワークが揃うとなんだか気持ちいいじゃないですか。私、昔からそういうの好きなんですよ。辞書は三省堂で揃えたりとか。技術書は同じシリーズのもの買うとか。

で、このたび業務で Amazon DynamoDB を使う機会がありまして。軽くググってみると、Spring Data DynamoDB ってのがあるそうじゃないですか。そこで、[Spring Data](https://spring.io/projects/spring-data) 公式ページに行ってみると、`Community modules` に`Spring Data DynamoDB` の文字が。

私は「Community modules」って文字に一抹の不安を感じつつも、採用に踏み切ったのでした。

# はじめの一歩
## ローカルで DynamoDB を起動
まずは、ローカルで試すために docker を利用。AWS公式がDockerイメージを提供してくれているんですね。ありがたい。

```
docker run -d --name dynamodb -p 8000:8000 amazon/dynamodb-local
```

これで、ローカルで確認が可能に。

## サンプルプロジェクトの作成
そして、Spring Initilizr で適当にプロジェクトを作る。
そして、公式からリンクされていた[ページ](https://github.com/boostchicken/spring-data-dynamodb)のREADMEを参考にしつつ、最初のサンプルを作成する。

### pom.xml に依存性を追加

```xml
<dependency>
  <groupId>io.github.boostchicken</groupId>
  <artifactId>spring-data-dynamodb</artifactId>
  <version>5.2.5</version>
</dependency>
```

### モデルとして User.java を作成
```Java
@Data
@NoArgsConstructor
@DynamoDBTable(tableName = "User")
public class User {
    @DynamoDBHashKey
    String id;
    @DynamoDBAttribute
    String firstName;
    @DynamoDBAttribute
    String lastName;
}
```

### UserRepository.java の作成
```Java
@EnableScan
public interface UserRepository extends CrudRepository<User, String> {
}
```

### DynamoDBConfig.java の作成
ローカルなのでシンプルに

```Java
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.myexample.springdatadynamodbtest.repositories")
public class DynamoDBConfig {

    @Bean
    public AmazonDynamoDB amazonDynamoDB() {
        return AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(
                new AwsClientBuilder.EndpointConfiguration("http://localhost:8000", "ap-northeast-1")
        ).build();
    }
}
```

### テストクラスの作成
```Java
@SpringBootTest
class UserRepositoryTest {

    @Autowired
    UserRepository sut;

    @Test
    void saveTest() {
        var sampleData = new User();
        sampleData.setId("1");
        sampleData.setFirstName("Hoge");
        sampleData.setLastName("Fuga");

        sut.save(sampleData);
    }

}
```

### テーブルを作る
aws cli でテーブルを作成

```shell
aws dynamodb create-table \
 --table-name User \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
  --endpoint-url http://localhost:8000
```

## テスト実行
成功！

# 立ちはだかる壁
上記ではユーザーがパーティションキーだけなので、ソートキーも含むテーブルを作ってみます。

## 複合プライマリキーを含むサンプルの作成
### Product.java

```Java
@Data
@NoArgsConstructor
@DynamoDBTable(tableName = "Product")
public class Product {
    @DynamoDBHashKey
    String id;
    @DynamoDBRangeKey
    String category;
    String name;
}
```

### ProductRepository.java

```Java
@EnableScan
public interface ProductRepository extends CrudRepository<Product, String> {
}
```

### テストコード作成

```Java
@SpringBootTest
class ProductRepositoryTest {
    @Autowired
    ProductRepository sut;

    @Test
    void saveUserTest() {
        var sampleData = new Product();
        sampleData.setId("1");
        sampleData.setCategory("Foo");
        sampleData.setName("Bar");

        sut.save(sampleData);
    }
}
```

### テーブルを作る
aws cli でテーブルを作成

```shell
aws dynamodb create-table \
 --table-name Product \
 --attribute-definitions AttributeName=id,AttributeType=S AttributeName=category,AttributeType=S \
 --key-schema AttributeName=id,KeyType=HASH AttributeName=category,KeyType=RANGE \
 --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
 --endpoint-url http://localhost:8000
```

## テスト実行
**失敗。**

```
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'productRepository' defined in com.myexample.springdatadynamodbtest.repositories.ProductRepository defined in @EnableDynamoDBRepositories declared on DynamoDBConfig: Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: org/springframework/data/repository/core/support/ReflectionEntityInformation
```

**「アイエエエ！？」「エラー！？エラーナンデ！？」「コワイ！」「ゴボボーッ！」**

## 再度挑戦

ネットの海をさまようこと数時間…以下の文書を発見した！

[Use Hash Range keys](https://github.com/derjust/spring-data-dynamodb/wiki/Use-Hash-Range-keys)

これによると、HashキーとRangキーでプライマリキーを作りたいときは、Spring Data のアノテーションの関係で、ちょっと特殊な書き方をしないといけないらしい。

これを参考に、以下の様に書き換える。

### Product.java

```Java
@Data
@NoArgsConstructor
@DynamoDBTable(tableName = "Product")
public class Product {
    @Id
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    ProductId productId; // IDを表現するクラスを用意する
    @DynamoDBHashKey
    String id;
    @DynamoDBRangeKey
    String category;
    String name;
}
```

### ProductId.java
プライマリキーを表現するクラスを用意する

```Java
@Data
@NoArgsConstructor
public class ProductId implements Serializable {
    @DynamoDBHashKey
    String id;
    @DynamoDBRangeKey
    String category;
}
```

## 再度テスト
成功！

…とはいえ、なんかスッキリしない。

わかりにくくないですか？
キーを表現するクラスを書くのはまだいいけど、`DynamoDBHashKey` アトリビュートは重複しているし。

ここに至って、ここまで難しいなら、AWS SDK を直接触った方がいいんじゃないか…？ と思い始めました。
調べてみると、「Amazon DynamoDB 拡張クライアント」なる言葉が。
え、ナニソレ？

# 異世界転生　(Amazon DynamoDB 拡張クライアントを使う)
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v2/developer-guide/examples-dynamodb-enhanced.html)によると、「Amazon DynamoDB 拡張クライアントは、AWS SDK for Java バージョン 2 (v2) の一部である高レベルのライブラリです」とのこと。
サンプル見てみたら非常にシンプルだったので、それを参考に、作り直してみた。

### pom.xml

```xml
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>dynamodb-enhanced</artifactId>
    <version>2.15.36</version>
</dependency>
```

### DynamoDBConfig.java

```Java
@Configuration
public class DynamoDBConfig {
    private final String dynamoDbEndPointUrl;

    public DynamoDBConfig() {
        this.dynamoDbEndPointUrl = "http://localhost:8000";
    }

    @Bean
    public DynamoDbClient getDynamoDbClient() {
        return DynamoDbClient.builder()
                .endpointOverride(URI.create(dynamoDbEndPointUrl))
                .build();
    }

    @Bean
    public DynamoDbEnhancedClient getDynamoDbEnhancedClient() {
        return DynamoDbEnhancedClient.builder()
                .dynamoDbClient(getDynamoDbClient())
                .build();
    }
}
```

### DynamoDBRepository.java
`UserRepository.java` / `ProductRepository.java` に変わって、テーブルアクセスを担うクラスを作成。型変数で複数の型に対応する。

```Java
@Component
public class DynamoDBRepository {

    final DynamoDbEnhancedClient client;

    @Autowired
    public DynamoDBRepository(DynamoDbEnhancedClient client) {
        this.client = client;
    }

    public <T> void save(T record, Class<T> recordClass) {
        String tableName = recordClass.getSimpleName();
        DynamoDbTable<T> table = client.table(tableName, TableSchema.fromBean(recordClass));
        table.putItem(record);
    }
}
```

### User.java

```Java
@Data
@DynamoDbBean
public class User {
    @Getter(AccessLevel.NONE)
    String id;
    String firstName;
    String lastName;

    @DynamoDbPartitionKey
    public String getId() {
        return id;
    }
}
```

`id` のGetterを書いているのは、DynamoDbPartitionKeyアノテーションが、フィールドに付与できないため。

### Product.java

```Java
@Data
@DynamoDbBean
public class Product {
    @Getter(AccessLevel.NONE)
    String id;
    @Getter(AccessLevel.NONE)
    String category;
    String name;

    @DynamoDbPartitionKey
    public String getId() {
        return id;
    }

    @DynamoDbSortKey
    public String getCategory() {
        return category;
    }
}
```

### テスト作成

```Java
@SpringBootTest
class ProductRepositoryTest {
    @Autowired
    DynamoDBRepository sut;

    @Test
    void saveUserTest() {
        var sampleData = new Product();
        sampleData.setId("1");
        sampleData.setCategory("Foo");
        sampleData.setName("Bar");

        sut.save(sampleData, Product.class);
    }

}
```

```Java
@SpringBootTest
class UserRepositoryTest {

    @Autowired
    DynamoDBRepository sut;

    @Test
    void saveUserTest() {
        var sampleData = new User();
        sampleData.setId("1");
        sampleData.setFirstName("Hoge");
        sampleData.setLastName("Fuga");

        sut.save(sampleData, User.class);
    }

}
```

## テスト実行
**すんなり通った**

# 技術チョイスはまちがっていたのか？
はい、間違ってました。
Spring が好きだからと言って、無条件に選ぶものではなかったですね。

では、Spring Data DynamoDB を使うことのメリットってなんだろう？

個人的には、Spring Data のメリットは、 Repository を自動生成してくれることと、異なるデータストアに対して同じアプローチで使えることだと思っているのだけど、KeyValue型の DynamoDB に対して、Spring Data が提供する CRUDRepository は、機能的に過剰な気がするので、無理して（クラスを増やしたりわかりにくいコードにしてまで）採用する必要はない感じだし、今の仕様通りに作ると他DBで使えなくなってしまう。

また、ライブラリの更新も滞り気味に見えました。Mavenリポジトリの最終更新も2020/01だったし、Spring Data DynamoDB について言及されているサイトでは「バージョン違いに気を付けろ」的に書かれていたりもする。
まあ、`Community modules` ですからね。

一方、AWS SDK for Java の DynamoDB Enhance は、天下のAmazon様がつくっているだけあって、更新も頻繁に行われている様子。
また、今回は使用しなかったが、非同期クライアントも用意されているようなので、最新のJavaとも相性がよさそう。

そしてアノテーションによるマッピング処理も、両者でそんなに違いは無い(むしろAWS SDKの方が少ない？)

と、いうわけで、餅は餅屋、AWSはAmazon製ライブラリを使いましょう、というのが今回の結論（Spring Data DynamoDB については）。


と言いつつ次は Soring Cloud AWS を試してみよう。