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
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行追加します。
plugins {
...
kotlin("plugin.allopen") version "1.4.32"
}
さらにallopenを記述します。
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Embeddable")
annotation("javax.persistence.MappedSuperclass")
}
モデル作成
Entityを作成します。
デフォルト値を持つフィールド変数はコンストラクタの後ろに定義します。コンストラクタを使用して定義する際、引数を省略できます。
Kotlinでは同じファイル内で簡潔なクラス宣言をグループ化することは普通なようです。
src/main/kotlin/com/example/blog/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
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
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
に変更してみます。
結果、問題なくテストを実行できました。
さいごに
長くなったのでこちらで一旦切ります。
今回はJPA周りを実装しました。