jOOQを使ったときに感じた2つのストレス
jOOQを使ったときにストレスを感じたことが2つあり、それを解消したい。
1つ目は、JSON型を含むテーブルをjOOQのコードジェネレーターで生成するとorg.jooq.JSON
となり、DBの入出力以外では扱いづらいこと。
2つ目は、IntelliJ IDEAでアプリ起動するたびにjOOQのコードジェネレーターが実行されるため起動時間が長いこと。
org.jooq.JSONをKotlinのクラスに変換する
JSON型を含むテーブルをjOOQのコードジェネレーターで生成するとorg.jooq.JSON
になる。DBへの入出力はorg.jooq.JSON
でもいいが、以下の点で問題が発生する。
- ビジネスロジック部分では
org.jooq.JSON
で扱いたくない - HTTPのレスポンスで
org.jooq.JSON
型のフィールドを含むクラスをJacksonはシリアライズできない
そのためdata classやListなどの普通のKotlinのシンプルな型に変換したい。
Kotlinの拡張関数で変換関数を実装する
JavaだとUtilクラスを使ってorg.jooq.JSON
と任意の型の相互変換メソッドを作るところだが、Kotlinでは拡張関数(extension function)を使って以下のように変換機能を作ると扱いやすくなる。
JSONExtension.kt
package com.example.jooqjson.domain.repository.util
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import org.jooq.JSON
private val objectMapper: ObjectMapper = ObjectMapper()
fun <T> JSON.to(clazz: Class<T>): T = objectMapper.readValue(this.data(), clazz)
fun <T> JSON.to(type: TypeReference<T>): T = objectMapper.readValue(this.data(), type)
fun Any.toJooqJSON(): JSON = JSON.json(objectMapper.writeValueAsString(this))
利用例
DDL, jOOQの生成したPOJO, HTTPレスポンスで使うdata class
MySQLに以下の定義のusersテーブルがあるとする。
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`roles` json DEFAULT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email_UNIQUE` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
これを元にjOOQのコードジェネレーターでPOJOを生成する。
data class Users(
var id: Int? = null,
var email: String? = null,
var password: String? = null,
var roles: JSON? = null
): Serializable {
// toString()は略
}
これを以下のレスポンス用data classに変換したい。
data class UserResponse(val id: Int, val email: String, val roles: List<Role>)
// Role定義は以下
enum class Role { ROLE_NORMAL, ROLE_ADMIN }
あるいは永続化するためにList<Role>
をorg.jooq.JSON
に変換してUsers
にセットしたい。
org.jooq.JSON
をList<Role>
に
org.jooq.JSON
をList<Role>
にするには、実装した拡張関数to
を使って以下のように書ける。
val users = usersRepository.selectById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
val userResponse = UserResponse(
id = users.id!!,
email = users.email!!,
roles = users.roles!!.to(object : TypeReference<List<Role>>() {})
)
List<Role>
をorg.jooq.JSON
に
List<Role>
をorg.jooq.JSON
にするには、実装した拡張関数toJooqJSON
を使って以下のように書ける。
val users = Users(
email = 略,
password = 略,
roles = listOf(Role.ROLE_NORMAL, Role.ROLE_ADMIN).toJooqJSON()
)
usersRepository.insert(users)
jOOQのコード生成設定
jOOQのコード生成機能を前提に記載しているため、コード生成の設定について記載する。
build.gradle.kts
まずはjOOQとjOOQのコード生成機能をSpring Boot, MySQL, Kotlinと一緒に使うための最低限のgradle設定を記載する。
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
id("nu.studer.jooq") version "6.0.1" // jooq code generator plugin
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-jooq")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("mysql:mysql-connector-java")
jooqGenerator("mysql:mysql-connector-java") // for jooq code generator
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
// 以下 jooq.configurations
jooq.configurations
先程あげた例を再現するために以下の2点が必要になる。
- POJOを生成するために
isPojos = true
にする - Kotlinを使用しているので
org.jooq.codegen.KotlinGenerator
でコード生成する
jooq.configrationsにコード生成の設定を記載する。
jooq {
configurations {
create("main") { // name of the jOOQ configuration
// set false when developing at local for application start up speed.
// set true when building jar for deployment.
val isGenerate = System.getenv("JOOQ_GENERATE")?.toBoolean() ?: false
generateSchemaSourceOnCompilation.set(isGenerate)
// generateSchemaSourceOnCompilation.set(true) // default (can be omitted)
jooqConfiguration.apply {
jdbc.apply {
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/test"
user = "test"
password = "test_password"
// properties.add(Property().withKey("ssl").withValue("true"))
}
generator.apply {
name = "org.jooq.codegen.KotlinGenerator"
database.apply {
name = "org.jooq.meta.mysql.MySQLDatabase"
inputSchema = "test"
}
generate.apply {
isRecords = true
// isImmutablePojos = true
isPojos = true
isDaos = true
}
target.apply {
packageName = "com.example.jooqjson.domain.repository.generated"
directory = "build/generated-src/jooq/main" // default (can be omitted)
}
strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy"
}
}
}
}
}
IntelliJ IDEAでSpring Bootを起動するときにjOOQのコードジェネレーターが実行されるのを防ぐ
IntelliJ IDEAでSpring Bootを起動をするたびに、jOOQのコード生成が実行されて速度が低下する点についてストレスを感じている。
起動のたびにコード生成されるのは以下の2点が原因。
- IntelliJ IDEAのアプリ起動クラスやJUnit起動メソッドのRun ConfigurationにBefore launchという設定があり、
Build
が初めから入っている
- jOOQの
generateSchemaSourceOnCompilation
がデフォルトtrue
でコンパイル時にコード生成されることになっている。
Before launch設定からBuild
を外すと、コードを修正して再起動するたびに自分でビルドしなくてはいけないので、逆に面倒。
jOOQのgenerateSchemaSourceOnCompilation
がfalse
になっているとCIサーバー等でjarを作るときに面倒。しかし、PC上で開発している分には、build/generated-src/jooq/main
に常に生成されたコードがあるはずで、DBに変更が入ったときかgradleのclean
をしたときのみjooq
タスクのgenerateJooq
で手動でコード生成すればいい。つまりCIサーバー等でjarを作るときのみtrue
に変えられるようにして、普段はfalse
になるようになっていればいい。
先程jooq.configrationsを記載したので再掲はしないが、以下の設定とコメント文が該当する。
// set false when developing at local for application start up speed.
// set true when building jar for deployment.
val isGenerate = System.getenv("JOOQ_GENERATE")?.toBoolean() ?: false
generateSchemaSourceOnCompilation.set(isGenerate)
// generateSchemaSourceOnCompilation.set(true) // default (can be omitted)
環境変数JOOQ_GENERATE=true
でgradleを実行すれば、build時にコード生成される。
環境
- Kotlin 1.6
- jOOQ 3.15.1
- Spring Boot 2.6.3