home

KotlinでSpring BootのWebアプリケーションを作成してみる(2)

2022-06-19T12:27:05+09:00

前提

前回に引き続きチュートリアルを実施してみます。

コーディングルールがわからないときはこちらを参照。(間違っていたら指摘いただけると幸いです。)

環境

  • macOS: 11.6.4
  • IntelliJ IDEA: 2021.2.3 (Community Edition)
  • Kotlin: 212-1.5.10-release-IJ5457.46
  • Gradle: 7.4.1
  • Java: temurin 17.0.3

手順

独自の拡張機能を作成する

以下のクラスを作成します。この関数は後ほど使うそうです。
(これで良いのでしょうか、少し不安です。)

src/main/kotlin/com/example/blog/Extensions.kt

Extensions.kt
package com.example.blog

import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.*

fun LocalDateTime.format() = this.format(englishDateFormatter)

private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }

private val englishDateFormatter = DateTimeFormatterBuilder()
        .appendPattern("yyyy-MM-dd")
        .appendLiteral(" ")
        .appendText(ChronoField.DAY_OF_MONTH, daysLookup)
        .appendLiteral(" ")
        .appendPattern("yyyy")
        .toFormatter(Locale.ENGLISH)

private fun getOrdinal(n: Int) = when {
    n in 11..13 -> "${n}th"
    n % 10 == 1 -> "${n}st"
    n % 10 == 2 -> "${n}nd"
    n % 10 == 3 -> "${n}rd"
    else -> "${n}th"
}

fun String.toSlug() = toLowerCase()
        .replace("\n", " ")
        .replace("[^a-z\\d\\s]".toRegex(), " ")
        .split(" ")
        .joinToString("-")
        .replace("-+".toRegex(), "-")

JPA を使用した永続性

Entityをopenに設定する

遅延フェッチを有効にするにはEntityをopenに設定する必要があるそうです。

build.gradle.ktsを編集します。
まずはpluginsに1行追加します。

build.gradle.kts
plugins {
  ...
  kotlin("plugin.allopen") version "1.4.32"
}

さらにallopenを記述します。

build.gradle.kts
allOpen {
  annotation("javax.persistence.Entity")
  annotation("javax.persistence.Embeddable")
  annotation("javax.persistence.MappedSuperclass")
}

モデル作成

Entityを作成します。
デフォルト値を持つフィールド変数はコンストラクタの後ろに定義します。コンストラクタを使用して定義する際、引数を省略できます。
Kotlinでは同じファイル内で簡潔なクラス宣言をグループ化することは普通なようです。

src/main/kotlin/com/example/blog/Entities.kt

Entities.kt
import com.example.blog.toSlug
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.ManyToOne

@Entity
class Article(
        var title: String,
        var headline: String,
        var content: String,
        @ManyToOne var author: User,
        var slug: String = title.toSlug(),
        var addedAt: LocalDateTime = LocalDateTime.now(),
        @Id @GeneratedValue var id: Long? = null)

@Entity
class User(
        var login: String,
        var firstname: String,
        var lastname: String,
        var description: String? = null,
        @Id @GeneratedValue var id: Long? = null)

リポジトリ作成

こちらも同じファイル内に2つのinterfaceを宣言しています。

src/main/kotlin/com/example/blog/Repositories.kt

Repositories.kt
package com.example.blog

import org.springframework.data.repository.CrudRepository

interface ArticleRepository : CrudRepository<Article, Long> {
    fun findBySlug(slug: String): Article?
    fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, Long> {
    fun findByLogin(login: String): User?
}

JPAのテストを作成

リポジトリが期待通り動作するかテストを作成します。
src/test/kotlin/com/example/blog/RepositoriesTests.kt

RepositoriesTests.kt
package com.example.blog

import Article
import ArticleRepository
import User
import UserRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
import org.springframework.data.repository.findByIdOrNull

@DataJpaTest
class RepositoriesTests @Autowired constructor(
        val entityManager: TestEntityManager,
        val userRepository: UserRepository,
        val articleRepository: ArticleRepository) {

    @Test
    fun `When findByIdOrNull then return Article`() {
        val juergen = User("springjuergen", "Juergen", "Hoeller")
        entityManager.persist(juergen)
        val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
        entityManager.persist(article)
        entityManager.flush()
        val found = articleRepository.findByIdOrNull(article.id!!)
        assertThat(found).isEqualTo(article)
    }

    @Test
    fun `When findByLogin then return User`() {
        val juergen = User("springjuergen", "Juergen", "Hoeller")
        entityManager.persist(juergen)
        entityManager.flush()
        val user = userRepository.findByLogin(juergen.login)
        assertThat(user).isEqualTo(juergen)
    }
}

またしてもエラーが発生しました。

2022-06-19 11:44:12.495  WARN 37201 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 42001, SQLState: 42001
2022-06-19 11:44:12.495 ERROR 37201 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQLステートメントに文法エラーがあります 
"insert into [*]user (description, firstname, lastname, login, id) values (?, ?, ?, ?, ?)"; 期待されるステートメント "identifier"
Syntax error in SQL statement "insert into [*]user (description, firstname, lastname, login, id) values (?, ?, ?, ?, ?)"; expected "identifier"; SQL statement:

どうやらUSERというワードはH2の予約語だそうなので、USERARTICLE_USERに変更してみます。
結果、問題なくテストを実行できました。

さいごに

長くなったのでこちらで一旦切ります。
今回はJPA周りを実装しました。

自己紹介

サムネイル

y5347M

バックエンドエンジニアをしています。