はじめに
この記事をご覧いただきありがとうございます。
ふだんは Spring Boot(Kotlin)でサーバーサイドの開発をしている筆者ですが、これまでデータベースといえば PostgreSQL 一択でした。
ところが最近JOINしたプロジェクトで DynamoDB を使うチャンスが訪れ、「やばい、全然わからない!」と焦り始め、ゼロから勉強をスタート。
せっかくなので学習過程で作ったシンプルな日報アプリを、チュートリアル形式でまとめてみました。
Docker で DynamoDB Local を立ち上げつつ、Spring Boot × Kotlin で実際に API を実装していく流れを紹介していきます。同じようにこれからDynamoDBを使用する機会に遭遇した方の一助になればと思います。
アプリのユースケース
- 社員が日報(作業内容やコメント)を投稿する
- 上司が社員ごとに投稿された日報を確認できる
ステップA:セットアップ(ローカル DynamoDB)
今回の使用技術は以下です。
項目 | 内容 |
---|---|
言語 | Kotlin |
フレームワーク | Spring Boot 3.x |
DynamoDB接続 | AWS SDK v2(Enhanced DynamoDB Client) |
DynamoDB環境 | ローカル(amazon/dynamodb-local Docker) |
1. プロジェクト構成と依存追加
build.gradle.kts
に以下を追加:
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.5.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.homidig"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
//以下の二つが必要
implementation(platform("software.amazon.awssdk:bom:2.27.17"))
implementation("software.amazon.awssdk:dynamodb-enhanced")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
BOM(Bill of Materials)は依存関係のバージョンを一括管理するための仕組みです
2. ローカルDynamoDBの起動(Docker)
docker-compose.yml
をプロジェクト直下に追加:
version: "3.8"
services:
dynamodb-local:
image: amazon/dynamodb-local
container_name: dynamodb-local
ports:
- "8000:8000"
command: "-jar DynamoDBLocal.jar -sharedDb"
起動コマンド:
docker compose up
これで http://localhost:8000
で DynamoDB が使えるようになります。
3. application.yml 設定
src/main/resources/application.yml
にローカル DynamoDB の設定:
aws:
dynamodb:
endpoint: http://localhost:8000
region: ap-northeast-1
table-name: Dailies
ステップB:DynamoDB テーブル定義 & エンティティ設計
テーブル使用は以下です。
項目 | 内容 |
---|---|
DynamoDBテーブル名 | Dailies |
PKの形式 |
"EMP#社員ID" (例:EMP#1234 ) |
SKの形式 |
"DATE#yyyy-MM-dd" (例:DATE#2024-01-01 ) |
GSI | 今回は未定義(後ほど追加予定) |
作成方法 | AWS CLI + docker compose 経由でスクリプト実行 |
1. DynamoDBエンティティ定義(Kotlin)
@DynamoDbBean
class DailyReportEntity (
@get:DynamoDbPartitionKey
@get:DynamoDbAttribute(value = "PK")
var pk: String,
@get:DynamoDbSortKey
@get:DynamoDbAttribute(value = "SK")
var sk: String,
@get:DynamoDbAttribute(value = "content")
var content: String,
@get:DynamoDbAttribute(value = "employeeName")
var employeeName: String
) {
constructor() : this(
pk = "", sk = "", content = "", employeeName = ""
)
companion object {
fun create(employeeId: String, date: OffsetDateTime, content: String, name: String): DailyReportEntity {
return DailyReportEntity(
pk = "EMP#$employeeId",
sk = "DATE#${date.toLocalDate()}",
content = content,
employeeName = name
)
}
}
}
2. Docker構成(docker-compose.yml
)
version: "3.8"
services:
dynamodb-local:
image: amazon/dynamodb-local
container_name: dynamodb-local
ports:
- "8000:8000"
command: "-jar DynamoDBLocal.jar -sharedDb"
setup-dynamodb:
image: amazon/aws-cli
depends_on:
dynamodb-local:
condition: service_healthy
environment:
- AWS_ACCESS_KEY_ID=dummyAccessKeyId
- AWS_SECRET_ACCESS_KEY=dummySecretAccessKey
- AWS_DEFAULT_REGION=ap-northeast-1
entrypoint: ["/bin/sh", "-c"]
volumes:
- ./init:/aws/init
command: |
sh ./init/create-table.sh
sleep 2
sh ./init/migration-data.sh
3. テーブル作成スクリプト(create-table.sh
)
DynamoDBInitializerなるものを作成する例も見ましたが、localと本番環境で同じスクリプトを使用できるようにshを作成する場合もあるようです。
aws dynamodb create-table \
--table-name Dailies \
--attribute-definitions \
AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
--key-schema \
AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--endpoint-url http://dynamodb-local:8000
4. 初期データ投入スクリプト(migration-data.sh
)
aws dynamodb batch-write-item \
--request-items file://./data/dailies.json \
--endpoint-url http://dynamodb-local:8000
5. 初期データ定義(dailies.json
)
{
"Dailies": [
{
"PutRequest": {
"Item": {
"PK": { "S": "EMP#001" },
"SK": { "S": "DATE#2024-01-01" },
"content": { "S": "今日は〇〇でした。" },
"employeeName": { "S": "田中太郎" }
}
}
}
]
}
以上でシードデータの流し込みまで完了です。
ステップC:Repository作成 & POST API 実装
項目 | 内容 |
---|---|
Repository層 | Enhanced DynamoDB Client で insert 処理を実装 |
APIエンドポイント | POST /api/daily |
リクエスト形式 |
employeeId , content , employeeName を受け取る |
処理内容 | DynamoDBに1件登録(PK・SKは# 付きで自動生成) |
1. Repository の作成
DailyReportRepository.kt
@Repository
class DairyReportRepositoryImpl(
enhancedClient: DynamoDbEnhancedClient,
@Value("\${aws.dynamodb.table-name}") private val tableName: String
) : DairyReportRepository {
private val table: DynamoDbTable<DailyReportEntity> = enhancedClient.table(
tableName,
TableSchema.fromBean(DailyReportEntity::class.java)
)
override fun save(
employeeId: String,
date: OffsetDateTime,
content: String,
employeeName: String
) {
val updateRequest = UpdateItemEnhancedRequest
.builder(DailyReportEntity::class.java)
.item(
DailyReportEntity.create(
employeeId = employeeId,
date = date,
content = content,
name = employeeName
)
)
.build()
table.updateItem(updateRequest)
}
2. リクエストDTOの定義
DailyReportRequest.kt
data class DailyReportRequest(
val employeeId: String,
val content: String,
val employeeName: String
)
3. Controller の作成
DailyReportController.kt
@RestController
@RequestMapping("/api/daily")
class DailyController(
private val dairyReportRepository: DairyReportRepository
) {
@PostMapping
fun saveDaily(
@RequestBody dailyReport: DailyReportRequest
): String {
dairyReportRepository.save(
dailyReport.employeeId,
OffsetDateTime.now(),
dailyReport.content,
dailyReport.employeeName,
)
return "保存しました"
}
}
動作確認(curl or HTTPクライアント)
curl -X POST http://localhost:8080/api/daily \
-H "Content-Type: application/json" \
-d '{
"employeeId": "1002",
"content": "設計レビュー",
"employeeName": "鈴木一郎"
}'
以上で日報の登録が完了しました。
ステップD:日報の取得(Query)の実装
DynamoDB から以下のような取得ができるようにします:
- ある社員の全日報を取得(
PK=EMP#社員ID
) - 特定期間に絞って取得(
SK=DATE#2025-01-01
〜DATE#2025-01-31
)
1. Kotlin実装(Repository)
@Repository
class DairyReportRepositoryImpl(
enhancedClient: DynamoDbEnhancedClient,
@Value("\${aws.dynamodb.table-name}") private val tableName: String
) : DairyReportRepository {
private val table: DynamoDbTable<DailyReportEntity> = enhancedClient.table(
tableName,
TableSchema.fromBean(DailyReportEntity::class.java)
)
//saveは省略
// ある社員の全日報を取得するクエリ
override fun findAllByEmployeeId(employeeId: String): List<DailyReportEntity> {
val entities: QueryEnhancedRequest = QueryEnhancedRequest
.builder()
.queryConditional(
keyEqualTo(
Key
.builder()
.partitionValue("EMP#$employeeId")
.build()
)
)
.build()
return table.query(entities).items().toList()
}
//期間で日報を取得するクエリ
override fun findByEmployeeIdAndDateRange(
employeeId: String,
from: LocalDate,
to: LocalDate
): List<DailyReportEntity> {
val entities: QueryEnhancedRequest = QueryEnhancedRequest
.builder()
.queryConditional(
sortBetween(
Key
.builder()
.partitionValue("EMP#$employeeId")
.sortValue("DATE#$from")
.build(),
Key
.builder()
.partitionValue("EMP#$employeeId")
.sortValue("DATE#$to")
.build()
)
)
.build()
return table.query(entities).items().toList()
}
}
2. Controllerの例
@GetMapping("/{employeeId}")
fun getAllDailyReports(
@PathVariable employeeId: String,
@RequestParam from: LocalDate?,
@RequestParam to: LocalDate?
): List<DairyReportResponse> {
val entities = if (from != null && to != null) {
dairyReportRepository.findByEmployeeIdAndDateRange(employeeId, from, to)
} else {
dairyReportRepository.findAllByEmployeeId(employeeId)
}
return entities.map {
DairyReportResponse(
it.pk,
it.content,
it.employeeName
)
}
}
注意点
-
QueryConditional.sortBetween()
は partitionKeyが一致している前提で使用する必要があります - DynamoDB は
Query
において 必ず partitionKey の指定が必要です(SQLでいうWHERE
相当)
おわりに
今回の記事では、Spring Boot × Kotlin で DynamoDB Local に接続し、日報を登録・取得するシンプルなアプリを作成しました。
次回は GSI を活用した検索機能や、より実用的なスキーマ設計にも挑戦してみたいと思います。