13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

atWareAdvent Calendar 2020

Day 4

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

Posted at

はじめに

(長文です。結論だけ知りたい方は最後だけ読んでください)

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

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

諸君、私は Spring が好きだ

好きなんですよ。Spring。

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

で、このたび業務で Amazon DynamoDB を使う機会がありまして。軽くググってみると、Spring Data DynamoDB ってのがあるそうじゃないですか。そこで、Spring Data 公式ページに行ってみると、Community modulesSpring 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

「アイエエエ!?」「エラー!?エラーナンデ!?」「コワイ!」「ゴボボーッ!」

再度挑戦

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

Use Hash Range keys

これによると、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 を試してみよう。

13
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?