はじめに
(長文です。結論だけ知りたい方は最後だけ読んでください)
このページにたどり着いた方はきっと、 Java で Amazon DynamoDB を使うことになって、普段使うフレームワークがが Spring なので、Spring のエコシステムに乗っかる形で DynamoDB が使えたら…とか考えている人だと思います。
私もそんな人間の一人でした。
そこで、同士達が、同じ苦労をしないように、ここに記録を残します。
諸君、私は Spring が好きだ
好きなんですよ。Spring。
なんで好きかって言われるとよくわからないんですが、Springって名前でフレームワークが揃うとなんだか気持ちいいじゃないですか。私、昔からそういうの好きなんですよ。辞書は三省堂で揃えたりとか。技術書は同じシリーズのもの買うとか。
で、このたび業務で Amazon DynamoDB を使う機会がありまして。軽くググってみると、Spring Data DynamoDB ってのがあるそうじゃないですか。そこで、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 で適当にプロジェクトを作る。
そして、公式からリンクされていたページのREADMEを参考にしつつ、最初のサンプルを作成する。
pom.xml に依存性を追加
<dependency>
<groupId>io.github.boostchicken</groupId>
<artifactId>spring-data-dynamodb</artifactId>
<version>5.2.5</version>
</dependency>
モデルとして User.java を作成
@Data
@NoArgsConstructor
@DynamoDBTable(tableName = "User")
public class User {
@DynamoDBHashKey
String id;
@DynamoDBAttribute
String firstName;
@DynamoDBAttribute
String lastName;
}
UserRepository.java の作成
@EnableScan
public interface UserRepository extends CrudRepository<User, String> {
}
DynamoDBConfig.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();
}
}
テストクラスの作成
@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 でテーブルを作成
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
@Data
@NoArgsConstructor
@DynamoDBTable(tableName = "Product")
public class Product {
@DynamoDBHashKey
String id;
@DynamoDBRangeKey
String category;
String name;
}
ProductRepository.java
@EnableScan
public interface ProductRepository extends CrudRepository<Product, String> {
}
テストコード作成
@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 でテーブルを作成
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
「アイエエエ!?」「エラー!?エラーナンデ!?」「コワイ!」「ゴボボーッ!」
再度挑戦
ネットの海をさまようこと数時間…以下の文書を発見した!
これによると、HashキーとRangキーでプライマリキーを作りたいときは、Spring Data のアノテーションの関係で、ちょっと特殊な書き方をしないといけないらしい。
これを参考に、以下の様に書き換える。
Product.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
プライマリキーを表現するクラスを用意する
@Data
@NoArgsConstructor
public class ProductId implements Serializable {
@DynamoDBHashKey
String id;
@DynamoDBRangeKey
String category;
}
再度テスト
成功!
…とはいえ、なんかスッキリしない。
わかりにくくないですか?
キーを表現するクラスを書くのはまだいいけど、DynamoDBHashKey
アトリビュートは重複しているし。
ここに至って、ここまで難しいなら、AWS SDK を直接触った方がいいんじゃないか…? と思い始めました。
調べてみると、「Amazon DynamoDB 拡張クライアント」なる言葉が。
え、ナニソレ?
異世界転生 (Amazon DynamoDB 拡張クライアントを使う)
公式ドキュメントによると、「Amazon DynamoDB 拡張クライアントは、AWS SDK for Java バージョン 2 (v2) の一部である高レベルのライブラリです」とのこと。
サンプル見てみたら非常にシンプルだったので、それを参考に、作り直してみた。
pom.xml
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb-enhanced</artifactId>
<version>2.15.36</version>
</dependency>
DynamoDBConfig.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
に変わって、テーブルアクセスを担うクラスを作成。型変数で複数の型に対応する。
@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
@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
@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;
}
}
テスト作成
@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);
}
}
@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 を試してみよう。