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)
1
2
3
4
5
6
7
8
9

プロジェクト作成

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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

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()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@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()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@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>
}
1
2
3
4
5
6
7
8
9

@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] ------------------------------------------------------------------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17