【GitHub】Kotlinの「色」が変わった話

「Swift/Kotlin愛好会 Advent Calendar 2022」の枠が空いていたので急遽書いてみました。

qiita.com


GitHubでは、画像のように、リポジトリ内の言語割合をカラフルに表示してくれます(画像はKotlinリポジトリより)。
この表示でのKotlinは紫っぽい色ですが、1年半ほど前まではオレンジっぽい色だったことをご存じでしょうか?

変更の経緯

この変更はZacSweersさんからの提案で行われました。

Zacさんはsquare/moshiのメンテナでもある超強いエンジニアです。
この件への最初の言及はこちらのツイートだったと記憶しています。

JavaKotlinは同じリポジトリに同居していることが多々有りますが、Javaは茶色で、当時のKotlinはオレンジっぽい色でした。
並べると……こう、大小ある感じで、見た目が良くありません。

ツイートより引用

ということで、Zacさんより以下のPRが出され、今の紫っぽい色に落ち着いたのでした。

github.com

後書き

変更から1年半程経過し、Kotlinを始めた当初から今の色だったという方も増えつつあるのではないかと思い、昔話としてこんな記事を書いてみました。
「Swift/Kotlin愛好会 Advent Calendar 2022」の枠はまだまだ有りますので、是非皆さんに書いて頂けると嬉しいです。

qiita.com

余談ですが、square/moshi及びZacSweers/MoshiXはコードやgradle周りが綺麗ということで個人的な推しリポジトリです。
JSONに関する一般的なライブラリですし、OSSのコードを読んで勉強してみたいという方にもおススメです。

【PostgreSQL】PL/pgSQLで全テーブルに一括で更新日時設定のトリガーをセットする

やること

テーブル別でUPDATED_ATカラムへのトリガー設定を行っている状況が有ったとします。

-- 関数作成
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
  RETURNS TRIGGER AS $$ BEGIN NEW.UPDATED_AT = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;

-- テーブル毎にトリガー設定
CREATE TABLE IF NOT EXISTS FOO(...);

CREATE TRIGGER set_timestamp
  BEFORE UPDATE ON FOO
  FOR EACH ROW
  EXECUTE PROCEDURE trigger_set_timestamp();

CREATE TABLE IF NOT EXISTS BAR(...);

CREATE TRIGGER set_timestamp
  BEFORE UPDATE ON BAR
  FOR EACH ROW
  EXECUTE PROCEDURE trigger_set_timestamp();

この書き方は可読性が悪く、設定漏れを起こしかねません。
そこで、全テーブルへの設定処理一括で行う形に修正を行います。

やったこと

PL/pgSQLを使って以下のような処理を作成しました。
管理用テーブル等を除いてUPDATED_ATカラムを持つテーブルを抽出し、それらに対してそれぞれトリガー設定を呼び出しています。

DO
$$
DECLARE
  -- postgresの管理用テーブルやflyway関連以外で、UPDATED_ATカラムを持つテーブルを抽出(その他除外したいテーブルはここに書く)
  has_updated_at_tables CURSOR FOR
    SELECT t.table_name FROM information_schema.tables t
      INNER JOIN information_schema.columns c ON c.table_name = t.table_name
        AND c.table_schema = t.table_schema
    WHERE t.table_schema = 'public'
      AND t.table_type = 'BASE TABLE'
      AND t.table_name != 'flyway_schema_history'
      AND c.column_name ILIKE 'UPDATED_AT'; -- ファイル上の定義は大文字だが、POSTGRES上は小文字扱いなため、ILIKEで検索している
  table_name VARCHAR;
BEGIN
  OPEN has_updated_at_tables;
  LOOP
    -- テーブル名を取得、取得できなくなればループ終了
    FETCH has_updated_at_tables INTO table_name;
      EXIT WHEN NOT FOUND;
    EXECUTE format(
      'CREATE TRIGGER set_timestamp
  BEFORE UPDATE ON %s
  FOR EACH ROW
  EXECUTE PROCEDURE trigger_set_timestamp()',
      table_name
    );
  END LOOP;
END
$$ LANGUAGE PLPGSQL;

これを用いると、先程のDDLは以下のようになります。

-- 関数作成
CREATE TABLE IF NOT EXISTS FOO(...);

CREATE TABLE IF NOT EXISTS BAR(...);

-- 関数作成
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
  RETURNS TRIGGER AS $$ BEGIN NEW.UPDATED_AT = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;

-- UPDATED_ATのトリガーを一括設定
DO
$$
DECLARE
  -- postgresの管理用テーブルやflyway関連以外で、UPDATED_ATカラムを持つテーブルを抽出(その他除外したいテーブルはここに書く)
  has_updated_at_tables CURSOR FOR
    SELECT t.table_name FROM information_schema.tables t
      INNER JOIN information_schema.columns c ON c.table_name = t.table_name
        AND c.table_schema = t.table_schema
    WHERE t.table_schema = 'public'
      AND t.table_type = 'BASE TABLE'
      AND t.table_name != 'flyway_schema_history'
      AND c.column_name ILIKE 'UPDATED_AT'; -- ファイル上の定義は大文字だが、POSTGRES上は小文字扱いなため、ILIKEで検索している
  table_name VARCHAR;
BEGIN
  OPEN has_updated_at_tables;
  LOOP
    -- テーブル名を取得、取得できなくなればループ終了
    FETCH has_updated_at_tables INTO table_name;
      EXIT WHEN NOT FOUND;
    EXECUTE format(
      'CREATE TRIGGER set_timestamp
  BEFORE UPDATE ON %s
  FOR EACH ROW
  EXECUTE PROCEDURE trigger_set_timestamp()',
      table_name
    );
  END LOOP;
END
$$ LANGUAGE PLPGSQL;

補足

自分は試していませんが、DEFAULT設定を上手く利用すればトリガー設定は省略できるかもしれません。

qiita.com

【Spring WebFlux】Cannot determine database's type as ConnectionFactory is not options-capable.への対処【R2DBC】

H2データベースを使ってR2DBCpostAllocate/preReleaseの挙動を確認するためのSpring WebFluxプロジェクトを作成していた所、Cannot determine database's type as ConnectionFactory is not options-capable. To be options-capable, a ConnectionFactory should be created with org.springframework.boot.r2dbc.ConnectionFactoryBuilderというエラーが出て詰まったので、対策用の備忘録です。
あまりよく確認していませんが、OptionsCapableConnectionFactoryを使うと上手くいくようでした。

import io.r2dbc.pool.ConnectionPool
import io.r2dbc.pool.ConnectionPoolConfiguration
import io.r2dbc.spi.ConnectionFactories
import io.r2dbc.spi.ConnectionFactory
import io.r2dbc.spi.ConnectionFactoryOptions
import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration

@Configuration
class R2dbcConfiguration : AbstractR2dbcConfiguration() {
    @Bean
    override fun connectionFactory(): ConnectionFactory {
        val h2UrlStr = "r2dbc:h2:mem:///testdb"

        val defaultConnectionFactory = ConnectionFactories.get(h2UrlStr)
        val config = ConnectionPoolConfiguration
            .builder(defaultConnectionFactory)
            .postAllocate { _ -> TODO() }
            .build()

        return OptionsCapableConnectionFactory(
            ConnectionFactoryOptions.parse(h2UrlStr),
            ConnectionPool(config)
        )
    }
}

PostgreSQLでやっている時にはConnectionPool(config)で上手くいくようだったため、何でH2の時だけこれが出たのかはよく分かっていません。

【Windows】IMEの入力言語が勝手に切り替わる問題への対処2種【PowerToys】

TL;DR

  • Windowsでは、特にゲーム中のキーボード操作によって入力言語が切り替わり、日本語入力できなくなる問題が有る
    • IMEが英語モードになってしまう
  • 入力言語切り替えのショートカットはデフォルトで2種類有り、それぞれを無効化することで切り替わりが発生しなくなる
    • 左Alt + Shift
    • Windows + Space

左Alt + Shiftへの対処

↓のサイトを参考に、キーボードの詳細設定 -> 入力言語のホットキー -> キーの詳細設定 -> キー シーケンスの変更 -> 入力言語の切り替え -> 割り当てなしに設定して無効化しました。

www.teradas.net

設定後の画像は以下の通りです。

Windows + Spaceへの対処

こちらは単純な設定では対処できませんが、Microsoftが公式に提供しているソフト(Microsoft PowerToys)を用いれば容易に無効化できます。
入手方法は幾つか存在しますが、Microsoft Store経由でインストールするのが簡単だと思います。

apps.microsoft.com

インストールした上で、Keyboard Manager -> ショートカットの再マップ -> 物理ショートカット: Windows + Space / マップ先: (任意のキー)と設定することで、入力言語切り替えを無効化出来ます。
画像ではWindows + Spaceの入力をSpaceの単体入力に変換しています。

後書き

Apex Legendsをプレイしている最中にこれが頻発して嫌になったので書きました。
何で同じ機能に複数ショートカットが有る上に、かつ片方は簡単に無効化できないようになってるんだろう。

【jOOQ】R2DBC接続でDSLContextを上手く管理する方法を考える【Spring】

以下の記事を書いていて、「DSLContextを使い捨てたら非効率なのでは?」と感じたので、管理方法を考えてみたメモです。
その他の便利要件も含めています。

あくまでメモなので、これがどれ位性能に影響するか等は未検証です。

qiita.com


前提

ソースコード

管理用クラスは以下の通りです。

import io.r2dbc.spi.ConnectionFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.awaitSingle
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.ResultQuery
import org.jooq.SQLDialect
import org.jooq.impl.DSL
import org.springframework.r2dbc.connection.ConnectionHolder
import org.springframework.stereotype.Component
import org.springframework.transaction.NoTransactionException
import org.springframework.transaction.reactive.TransactionSynchronizationManager
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono

@Component
class JooqContext(private val cfi: ConnectionFactory) {
    companion object {
        // TransactionSynchronizationManagerのリソースにDSLContextをバインドする際に用いるキー
        private const val DSL_CONTEXT_KEY = "DSLContext"
    }

    // 読み込み時にトランザクションが開始されていなければ使うDSLコンテキスト
    private val defaultContextForRead = DSL.using(cfi, SQLDialect.H2).dsl()

    // DSLContextは一々インスタンスを使い捨てるとオーバーヘッドが大きそうなため、コンテキストにバインドしてそれを使い回す
    private fun getDslContext(manager: TransactionSynchronizationManager): DSLContext {
        manager.getResource(DSL_CONTEXT_KEY)?.apply { return (this@apply as DSLContext) }

        val connection = (manager.getResource(cfi) as ConnectionHolder).connection
        val dslContext = DSL.using(connection, SQLDialect.H2).dsl()

        manager.bindResource(DSL_CONTEXT_KEY, dslContext)

        return dslContext
    }

    // テストでモックするため公開
    fun getCurrentDsl(): Mono<DSLContext> = TransactionSynchronizationManager
        .forCurrentTransaction() // forCurrentTransactionはトランザクション非開始でエラーが生じる
        .map { getDslContext(it) }

    // readは基本的に1件以上の取得が期待されるため、fluxを返す
    fun <R : Record> read(query: DSLContext.() -> ResultQuery<R>): Flow<R> = getCurrentDsl()
        // read時はトランザクション非開始でデフォルトにフォールバック
        .onErrorReturn(NoTransactionException::class.java, defaultContextForRead)
        .flatMapMany { query(it) }
        .asFlow()

    // write時はトランザクションを強制するため、トランザクション非開始エラーを処理しない
    suspend fun <R : Record> write(query: DSLContext.() -> ResultQuery<R>): R = getCurrentDsl()
        .flatMap { query(it).toMono() }
        .awaitSingle()

    fun <R : Record> writeMany(query: DSLContext.() -> ResultQuery<R>): Flow<R> = getCurrentDsl()
        .flatMapMany { query(it) }
        .asFlow()
}

利用側は以下のようになります。
DSLContextMono/Fluxの生成処理が管理クラス側に隠ぺいできた分だけ利用側がすっきりしています。

import org.jooq.generated.tables.records.FooTableRecord
import org.jooq.generated.tables.references.FOO_TABLE
import org.springframework.stereotype.Repository

@Repository
class Repository(private val ctxt: JooqContext) {
    suspend fun save(value: String): FooTableRecord {
        return ctxt.write { insertInto(FOO_TABLE).values(value).returning() }
            .map { it.into(FOO_TABLE) }
    }
}

解説

追記: ConnectionFactoryUtils.getConnectionを用いる際のコネクションリークについて

以下の解説ではConnectionFactoryUtils.getConnectionの利用について触れていますが、これを使う際には適切にcloseしなければコネクションリークが発生する点にご注意下さい。
詳しくは以下にまとめています。

qiita.com


コネクションやDSLContextの管理はTransactionSynchronizationManagerを直接利用するようにしています。
この形にしている理由は以下の通りです。

  • トランザクション無しで書き込もうとしたらエラーにしたい」を実現するのに都合がいい
  • TransactionSynchronizationManagerにリソースをバインドすることで、DSLContextを使い回せる

特にコネクション取得について、Qiitaの方ではConnectionFactoryUtils.getConnectionを用いていましたが、例外もしっかりケアされていたため、安全性だけ考えるとこちらの方が良いと思います。
この記事で紹介したコードはあくまで自分のユースケースでちゃんと動きそうという程度しか見ていません。 docs.spring.io

【Spring】JDBC接続で、suspend関数に対してTransactionalアノテーションを利用してのロールバックが効くか確認する【Kotlin】

お詫び

過去公開していた記事では、Spring WebFluxで複数リクエストを同時に処理する場合を考慮していませんでした。
JDBC接続 x Spring WebFluxトランザクション管理をすると壊れる可能性が有るため、やらないことをお勧めします。

R2DBC接続でトランザクションが効かない!」という方は下記の記事が参考になるかもしれません。

qiita.com

何故壊れるのか

トランザクション管理にThreadLocalが利用されているためです。

github.com

Spring WebMvcでは、リクエスト毎にスレッドが生成されて利用されるため、ThreadLocalを使っても1リクエスト1スレッドで処理できます。
一方、Spring WebFluxでは、固定数のスレッドで全てのリクエストを処理します。
つまり、別リクエストの影響を受けて壊れる可能性が有るということです。

ThreadLocalの利用に関してはProject ReactorSpring WebFluxの基盤)のサイトでも言及があります。

projectreactor.io


TL;DR

  • 登録関数とそれを呼び出す関数がそれぞれsuspend関数か否かに関わらず、ロールバックは成功するようだった
    • H2Postgres両方でこの結果となった
  • ただし、呼び出し関数がsuspend関数で、coroutineScopeを切って登録関数を呼び出した際に不可解な挙動が生じたため、注意が必要

文脈

jOOQ/R2DBCロールバックできることが確認できたので、この文脈は読み飛ばして下さい。

qiita.com

自分が調査した限り、Spring 2.7.2で、jOOQ 3.16.4R2DBC接続で利用する場合、Transactionalアノテーションを利用してのロールバックは効きません。 この問題は以前のバージョンにも存在しています。

原因に関して調査したものの、明確な解決策を見つけることはできませんでした。 そもそも原因がjOOQなのかすら分かっていませんが、一応jOOQ 3.18に向けて幾つかの作業は進行中のようです。

github.com

jOOQは使いたい、でもTransacionalを使うならR2DBC接続にはできない……ということで、JDBC接続のjOOQsuspend関数から使った場合にどこまでトランザクションが効くか確認します。 この検証についてjOOQは多分あまり関係ないですが、一応自分の用途に合わせてjOOQを嚙ませています。

検証内容

検証に用いたプロジェクトは下記リポジトリに置いてあります。

github.com

検証内容としては、登録関数とそれを呼び出す関数がそれぞれsuspend関数か否かでTransactionalの挙動がどう変化するかを確認します。
確認方法は以下の通りです。

  1. テーブルに適当な値を登録し、登録が成功したことを確認する
  2. RuntimeExceptionthrowする(比較用にthrowしないパターンも用意する)
  3. レコードを取得してみて、ロールバックされているかを確認する

まず、登録関数と呼び出し関数のコードはそれぞれ以下の通りです。

// 登録関数
import org.jooq.DSLContext
import org.jooq.generated.tables.references.FOO_TABLE
import org.springframework.stereotype.Repository

@Repository
class Repository(private val create: DSLContext) {
    fun findAll() = create.selectFrom(FOO_TABLE).fetch()

    fun nonSuspendSave(value: String) = create.insertInto(FOO_TABLE).values(value).returning().fetchAnyInto(FOO_TABLE)

    suspend fun suspendSave(value: String) =
        create.insertInto(FOO_TABLE).values(value).returning().fetchAnyInto(FOO_TABLE)
}
// 呼び出し関数
import kotlinx.coroutines.runBlocking
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

object TestEx : RuntimeException("fail for test")

@Suppress("FunctionName")
@Component
@Transactional
class Caller(private val repository: Repository) {
    private fun getFuncName() = Throwable().stackTrace.let { it[1].methodName }

    // region 呼び出し元が非suspend
    fun nonSuspend_nonSuspend() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.nonSuspendSave(funcName)}")
    }

    fun nonSuspend_nonSuspend_fail() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.nonSuspendSave(funcName)}")
        throw TestEx
    }

    fun nonSuspend_suspend() {
        val funcName = getFuncName()
        println("$funcName on save\n${runBlocking { repository.suspendSave(funcName) }}")
    }

    fun nonSuspend_suspend_fail() {
        val funcName = getFuncName()
        println("$funcName on save\n${runBlocking { repository.suspendSave(funcName) }}")
        throw TestEx
    }
    // endregion

    // region 呼び出し元がsuspend
    suspend fun suspend_nonSuspend() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.nonSuspendSave(funcName)}")
    }

    suspend fun suspend_nonSuspend_fail() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.nonSuspendSave(funcName)}")
        throw TestEx
    }

    suspend fun suspend_suspend() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.suspendSave(funcName)}")
    }

    suspend fun suspend_suspend_fail() {
        val funcName = getFuncName()
        println("$funcName on save\n${repository.suspendSave(funcName)}")
        throw TestEx
    }
    // endregion
}

データベースはH2を使っています。

検証結果

以下のテストから呼び出して確認を行いました。

import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
private class CallerTest @Autowired constructor(
    val caller: Caller,
    val repository: Repository
) {
    private fun <T> exec(thrower: () -> T?): Any? = try { thrower() } catch (e: TestEx) { e }

    @BeforeEach
    fun callSaves() {
        exec { caller.nonSuspend_nonSuspend() }
        exec { caller.nonSuspend_nonSuspend_fail() }
        exec { caller.nonSuspend_suspend() }
        exec { caller.nonSuspend_suspend_fail() }

        exec { runBlocking { caller.suspend_nonSuspend() } }
        exec { runBlocking { caller.suspend_nonSuspend_fail() } }
        exec { runBlocking { caller.suspend_suspend() } }
        exec { runBlocking { caller.suspend_suspend_fail() } }
    }

    @Test
    fun print() {
        println("result:\n${repository.findAll()}")
    }
}

結果は以下のようになりました。
fail(= throwする)パターンは全てロールバックされていることが分かります。

result:
+------------------------------+
|TEXT_VALUE                    |
+------------------------------+
|nonSuspend_nonSuspend         |
|nonSuspend_suspend            |
|suspend_nonSuspend$suspendImpl|
|suspend_suspend$suspendImpl   |
+------------------------------+

懸念点が無いでもありませんが、一応普通にやっている間はロールバックされること確認できました。
また、コードは載せられませんが、PostgreSQLでやった場合にも同様の結果となりました。

補足: 呼び出し関数がsuspend関数で、coroutineScopeを切って登録関数を呼び出した際の不可解な挙動について

登録関数を以下のように変更した所、2件登録したレコードがどちらもロールバックされないという結果になりました。

suspend fun suspend_suspend_fail() {
    val funcName = getFuncName()
    println("$funcName on save\n${repository.suspendSave(funcName)}")
    coroutineScope {
        launch { println("$funcName on save2\n${repository.suspendSave(funcName + "2")}") }
    }.join()

    throw TestEx
}

coroutineScoperunBlockingで動かすなどすれば2件ともロールバックされているようでした(= 呼び出し関数が非suspend関数の場合にはrunBlockingせざるを得ないため、この現象は発生しない)。

coroutineScopeの内側はともかく、外側までロールバックされなくなるというのは少し直感に反する挙動に感じましたが、coroutineScopeを切って操作するというのがトランザクション処理と相性が悪そうなのはそうなので、やらないよう気を付ける必要が有るかなと思っています。

【jOOQ】etiennestuder/gradle-jooq-plugin(nu.studer.jooq)でコード生成時にjava.lang.ClassNotFoundException: jakarta.xml.bind.annotation.XmlSchemaが出る状況への対処

やり方

dependenciesjooqGenerator("jakarta.xml.bind:jakarta.xml.bind-api:3.0.0")を追加すれば生成が通りました。

dependencies {
    /* 略 */
    jooqGenerator("jakarta.xml.bind:jakarta.xml.bind-api:3.0.0")
    /* 略 */
}

補足

implementationだと通りませんでした。
バージョンについては、まず./gradlew dependenciesの出力からjakarta.xml.bindが含まれるものを探して、次にjakarta.xml.bind:jakarta.xml.bind-api:3.0.0 -> 2.3.3 (*)みたいな感じでバージョンが下がってるっぽい所を見つけ、より上のバージョンに合わせました。

バージョン類

  • nu.studer.jooq:7.1.1
  • org.jooq:jooq:3.16.4