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?

More than 3 years have passed since last update.

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

Posted at

前提

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

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

環境

  • 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周りを実装しました。

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?