0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初めてのDynamoDB:Spring Boot(kotlin)で作る日報アプリ

Posted at

はじめに

この記事をご覧いただきありがとうございます。
ふだんは 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-01DATE#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 を活用した検索機能や、より実用的なスキーマ設計にも挑戦してみたいと思います。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?