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.JSONList<Role>

org.jooq.JSONList<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のgenerateSchemaSourceOnCompilationfalseになっていると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