KotlinでSpring BootのWebアプリケーションを作成してみる（２）

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

https://qiita.com/y5347M/items/17a3198a2eb553844634

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

https://runebook.dev/ja/docs/kotlin/docs/reference/coding-conventions

# 環境
- 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`
```kotlin: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`
```kotlin: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`
```kotlin: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`
```kotlin: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の予約語だそうなので、`USER`を`ARTICLE_USER`に変更してみます。
結果、問題なくテストを実行できました。

https://github.com/h2database/h2database/issues/3363

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