DynamoDB LocalをSpring Bootで使う

概要

DynamoDB Localを、Spring Bootで使うメモ。

Spring DataのCrudRepositoryを使用した、リポジトリクラスの定義と、自動生成や、Spring BootのAuto configurationの仕組みを組み合わせて、プロダクションと、テストで、データストアの使い分けができるようにしていきます。

この手順で使用したコードは、以下に公開しているので、こちらも参考にしてください。
https://github.com/yo1000/ddb-local/tree/e9eb5812f6/ddb-local-spring-boot

要件

環境

今回の作業環境は以下のとおりです。

  • Java 1.8.0_131
  • Kotlin 1.2.10
  • DynamoDB SDK 1.11.263
  • DynamoDB Local 1.11.86
  • Spring Boot 2.0.0.M7
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.5
BuildVersion:	16F2073

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

プロジェクト作成

Spring Initializrでプロジェクトを作成し、必要な依存を設定します。

Spring Initializr

Initializrテンプレート内には、DynamoDB用の依存が用意されていないので、 ここではとくに依存を選択せずに、プロジェクトを作成していきます。

$ curl https://start.spring.io/starter.tgz \
  -d dependencies="" \
  -d language="kotlin" \
  -d javaVersion="1.8" \
  -d packaging="jar" \
  -d bootVersion="2.0.0.M7" \
  -d type="maven-project" \
  -d groupId="com.yo1000" \
  -d artifactId="kc-resource-server" \
  -d version="1.0.0-SNAPSHOT" \
  -d name="ddb-local-spring-boot" \
  -d description="DynamoDB Local Demo" \
  -d packageName="com.yo1000.dynamo.local" \
  -d baseDir="ddb-local-spring-boot" \
  -d applicationName="DdbLocalSpringBootApplication" \
  | tar -xzvf -

pom.xml

DynamoDB、およびDynamoDB Localを使用するのに必要な依存を追加していきます。

ビルドプラグインの設定については、以前のポスト(DynamoDB Local を使用したテスト)で触れているので、内容を把握したい場合には、そちらを確認してください。

pom.xml掲載の後に、その他の要点をまとめます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yo1000</groupId>
    <artifactId>ddb-local-spring-boot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>ddb-local-spring-boot</name>
    <description>DynamoDB Local Spring Boot Example</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.M7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <kotlin.version>1.2.10</kotlin.version>
        <dynamodb.version>[1.11,2.0)</dynamodb.version>
        <dynamodblocal.version>[1.11,2.0)</dynamodblocal.version>
        <sqlite4java.version>1.0.392</sqlite4java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jre8</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-dynamodb</artifactId>
            <version>${dynamodb.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.derjust</groupId>
            <artifactId>spring-data-dynamodb</artifactId>
            <version>5.0.1</version>
            <exclusions>
                <exclusion>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>aws-java-sdk-dynamodb</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>DynamoDBLocal</artifactId>
            <version>${dynamodblocal.version}</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>com.amazonaws</groupId>
                    <artifactId>aws-java-sdk-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.almworks.sqlite4java</groupId>
            <artifactId>sqlite4java</artifactId>
            <version>${sqlite4java.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>kotlin-maven-plugin</artifactId>
                <groupId>org.jetbrains.kotlin</groupId>
                <configuration>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <argLine>-Dsqlite4java.library.path=${basedir}/target/dependencies</argLine>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>process-test-resources</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/dependencies</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>

        <repository>
            <id>dynamodb-local-tokyo</id>
            <name>DynamoDB Local Release Repository</name>
            <url>https://s3-ap-northeast-1.amazonaws.com/dynamodb-local-tokyo/release</url>
        </repository>
        <repository>
            <id>dynamodb-local-oregon</id>
            <name>DynamoDB Local Release Repository</name>
            <url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>

spring-data-dynamodb

サードパーティ製のDynamoDB用Spring Dataライブラリです。リポジトリクラスの実装等が非常に簡単になります。

コンフィグレーション

プロダクション

コード例の後に、要点をまとめます。

package com.yo1000.dynamo.local

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableDynamoDBRepositories(basePackages = ["com.yo1000.dynamo.local.repository"])
class DynamoDBConfiguration {
    @Bean
    @ConditionalOnMissingBean
    fun amazonDynamoDB(): AmazonDynamoDB {
        return AmazonDynamoDBClientBuilder.standard().build()
    }
}

@ConditionalOnMissingBean

アプリケーション起動時、DIコンテナ上にAmazonDynamoDBインスタンスが見つからない場合に、このメソッドの戻り値をDIコンテナに登録してくれるようになります。既に登録済みのインスタンスを見つけた場合はこれをスキップします。

テスト実行時など、DynamoDBを参照できないロケーションでこのメソッドが実行されると、例外をスローしてしまうため、プロダクション環境以外で実行されないように、@ConditionalOnMissingBeanアノテーションを設定しておきます。

@EnableDynamoDBRepositories

指定しておくと、@EnableScanアノテーションの付けられたリポジトリインターフェースを自動的に実装し、DIコンテナに自動登録してくれるようになります。自動実装されるインターフェース上のメソッドは、JPA による永続化メソッド群の命名規則に 従う必要があります。

テスト

コード例の後に、要点をまとめます。

package com.yo1000.dynamo.local

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB
import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary

@Configuration
@EnableDynamoDBRepositories(basePackages = ["com.yo1000.dynamo.local.repository"])
class TestDynamoDBConfiguration {
    @Bean
    fun amazonDynamoDB(): AmazonDynamoDB {
        return DynamoDBEmbedded.create().amazonDynamoDB()
    }
}

amazonDynamoDB(): AmazonDynamoDB

プロダクション側で設定したものと、同じクラスによるDIコンテナへの登録メソッドです。プロダクション側のメソッドに、@ConditionalOnMissingBeanアノテーションを付けているので、こちらのメソッドによるDIコンテナへの登録が優先され、テスト時にはこちらの定義が使用されるようになります。

テスト用に、DynamoDBEmbeddedインスタンスを返却するようにしているので、テスト実行時にはDynamoDB Localが使用されるようになちます。

リポジトリ

データ

コード例の後に、要点をまとめます。

package com.yo1000.dynamo.local.repository

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable

@DynamoDBTable(tableName = "Stationary")
class Stationary(
        @get:DynamoDBHashKey
        var id: String = "",
        @get:DynamoDBAttribute
        var name: String = ""
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Stationary

        if (id != other.id) return false
        if (name != other.name) return false

        return true
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + name.hashCode()
        return result
    }
}

@DynamoDBTable(tableName = "Stationary")

このクラスインスタンスがDynamoDBの永続化対象であることをマークします。

@get:DynamoDBHashKey

DynamoDBの各種アノテーションはGetterメソッドに設定して使用します。このGetterメソッドが、主となる検索キーであることをアノテーションでマークします。

@get:DynamoDBAttribute

このGetterメソッドが、DynamoDBで永続化される属性であることをマークします。

var

DynamoDBとマッピングするクラスの各フィールドは、(アノテーションはGetterだけにしか付けないにも関わらず)対応するGetterとSetterの両方が必要になるため、フィールドはvarで宣言する必要があります。

equals, hashCode

データの検索時に、これらメソッドが使用されるため、実装しておく必要があります。

CrudRepository

コード例の後に、要点をまとめます。

package com.yo1000.dynamo.local.repository

import org.socialsignin.spring.data.dynamodb.repository.EnableScan
import org.springframework.data.repository.CrudRepository

@EnableScan
interface StationaryRepository : CrudRepository<Stationary, String> {
    fun findByName(name: String): List<Stationary>
}

@EnableScan

このアノテーションでクラスをマークしておくと、コンフィグレーションクラスの、@EnableDynamoDBRepositoriesに応じて、リポジトリクラスが自動実装されるようになります。

テスト

テストを実行して、結果を確認します。

$ ./mvnw clean test

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.651 s - in com.yo1000.dynamo.local.repository.StationaryRepositoryTest
2018-01-16 00:47:12.293  INFO 20085 --- [       Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2e570ded: startup date [Tue Jan 16 00:47:08 JST 2018]; root of context hierarchy
2018-01-16 00:47:12.296  INFO 20085 --- [       Thread-2] c.a.s.d.l.shared.access.LocalDBClient    : Shutting down
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.825 s
[INFO] Finished at: 2018-01-16T00:47:12+09:00
[INFO] Final Memory: 65M/645M
[INFO] ------------------------------------------------------------------------
avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII