【Gradle】Kotlinのプラグインバージョンを下げるとCaused by: java.lang.NoSuchFieldError: KOTLIN_STAT_LABEl_PROPERTYでsyncが失敗する状況への対処

TL;DR

  • 自分が遭遇した問題の原因はorg.jmailen.kotlinterプラグインKotlinバージョン間の互換性が無いことだった
  • プラグインバージョンを確認するのが良いかも

起きたこと

Kotlinプラグインのバージョンを1.7.21 -> 1.5.32に変更した所、Caused by: java.lang.NoSuchFieldError: KOTLIN_STAT_LABEl_PROPERTYsyncが失敗するようになりました。
ログ・スタックトレースは以下の通りでした。

ログ・スタックトレース

Execution failed for task ':prepareKotlinBuildScriptModel'.
> Failed to notify task execution listener.
   > KOTLIN_STAT_LABEl_PROPERTY

* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':prepareKotlinBuildScriptModel'.
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:94)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
    at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
Caused by: org.gradle.internal.event.ListenerNotificationException: Failed to notify task execution listener.
    at org.gradle.internal.event.AbstractBroadcastDispatch.dispatch(AbstractBroadcastDispatch.java:89)
    at org.gradle.internal.event.BroadcastDispatch$CompositeDispatch.dispatch(BroadcastDispatch.java:346)
    at org.gradle.internal.event.BroadcastDispatch$CompositeDispatch.dispatch(BroadcastDispatch.java:249)
    at org.gradle.internal.event.ListenerBroadcast.dispatch(ListenerBroadcast.java:141)
    at org.gradle.internal.event.ListenerBroadcast.dispatch(ListenerBroadcast.java:37)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy88.afterExecute(Unknown Source)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
    at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
Caused by: java.lang.NoSuchFieldError: KOTLIN_STAT_LABEl_PROPERTY
    at org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildEsStatListener$label$2.invoke(KotlinBuildEsStatListener.kt:32)
    at org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildEsStatListener$label$2.invoke(KotlinBuildEsStatListener.kt:32)
    at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
    at org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildEsStatListener.getLabel(KotlinBuildEsStatListener.kt:32)
    at org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildEsStatListener.reportData(KotlinBuildEsStatListener.kt:57)
    at org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildEsStatListener.afterExecute(KotlinBuildEsStatListener.kt:73)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.event.AbstractBroadcastDispatch.dispatch(AbstractBroadcastDispatch.java:43)
    at org.gradle.internal.event.BroadcastDispatch$SingletonDispatch.dispatch(BroadcastDispatch.java:245)
    at org.gradle.internal.event.BroadcastDispatch$SingletonDispatch.dispatch(BroadcastDispatch.java:157)
    at org.gradle.internal.event.AbstractBroadcastDispatch.dispatch(AbstractBroadcastDispatch.java:61)
    at org.gradle.internal.event.BroadcastDispatch$CompositeDispatch.dispatch(BroadcastDispatch.java:346)
    at org.gradle.internal.event.BroadcastDispatch$CompositeDispatch.dispatch(BroadcastDispatch.java:249)
    at org.gradle.internal.event.ListenerBroadcast.dispatch(ListenerBroadcast.java:141)
    at org.gradle.internal.event.ListenerBroadcast.dispatch(ListenerBroadcast.java:37)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy88.afterExecute(Unknown Source)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
    at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
    at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)

原因・対処

自分のプロジェクトでは、org.jmailen.kotlinterプラグインKotlinバージョン間の互換性が無いことが原因でした。
そこで、以下の互換表に従って、org.jmailen.kotlinterのバージョンを3.9.0に下げた所、問題が解決しました。

github.com

1年のアウトプットを振り返る

今年もやります。
以下は去年分です。

wrongwrong163377.hatenablog.com

アウトプットまとめ

まず今年1年で行った主なアウトプットです。

外部OSSへのコントリビュート

全体を通して見ると、特にJetBrains/kotlinへ重要な内容で貢献できたことが良かったです。

考えてみると、色々なリポジトリへの貢献を続けて来たこともあって、「ITサービスを利用する全ての人間は自分の携わったプログラムを少なくとも間接的に動かす機会が有る」と言えそうな状態になっており、「OSS活動ってすげえなー」と改めて思います(自分の貢献はちょっとだけお手伝いした程度に過ぎませんが)。
来年も引き続き頑張っていこうと思います。

Kotlin関係

今年大きかったのは、JetBrains/kotlin(正確にはkotlin-reflect)に幾つかのPRをマージして頂けたことです。

qiita.com wrongwrong163377.hatenablog.com

KFunction.callByは数多くのライブラリ・フレームワークで利用される機能で、これの高速化はKotlinの弱点の1つであるリフレクションの遅さを緩和するものです。
value class絡みのバグ修正も、これによってkotlin-reflectに依存するライブラリが実質的にvalue classサポートを行えるようになったという内容です。
どちらも非常に重要な内容であり、メンテナの助力も頂いたとは言え、このような変更を自力で行えたのはとてもいい経験でした。

jackson-module-kotlin関係

去年1年間は多くの活動を行なったjackson-module-kotlin関係ですが、年始に少し貢献した後はメンテな不在となってしまい、一切貢献できませんでした。
ほぼ1年メンテナ不在の状況が続いてしまったこともあり、jackson-module-kotlin関係は正直プロジェクトの今後に大きな不安が有ります。

正直メンテナを引き受けようかと思ったことも有りますが、仕事の状況や自由にOSS活動したかったこともあって手を上げられないままでした。
あるいは、jackson-module-kotlin全体を書き直すようなプロジェクトをやりたいとはずっと思っているので、来年は何かしらのアクションを起こせるといいなと思っています。

その他

幾つかのリポジトリにはソースコードで貢献したり、発見したバグの報告をしたりしていました。
大きな内容を実現するのもいいですが、こう言った細かい内容での貢献も続けて行きたいですね。

ブログ

2017年からの年間ブログ数50本記録が途絶えてしまいました。
割と継続していた記録が途切れてしまったのは結構残念に感じます。

理由はいくつか有りますが、初心者を脱却しつつあってブログに書く程の何かがあまり無いことと、OSSへの貢献比重を高めると新しい知識に触れる機会が少ないというのが大きい気がしています。
後はAPEXですかね……。

来年もどちらかと言えばOSS活動の方に注力したく、正直本数は戻せない気がしています。

終わりに

今年はAPEXにハマっていたせいでアウトプットが滞っていた感が有ります。
とは言えOSSへの貢献という点では大きな仕事ができたと思うので、そこは良かったです。

仕事の方は、昨年末に転職してからひたすらSpring WebFluxに向き合った(というか振り回され続けた)1年でした。

技術面では特にReactor周りの難しさが辛く、改善するユースケースもそれほど多くない感が有って、「これじゃあSpring WebFluxが流行ることは無いなあ」と感じました。
来年9月には次のLTSとなるJDK21が控えており、ここでProject Loomの仮想スレッドが正式化するようなことが有ればいよいよSpring WebFlux要らない子化するんじゃないかという不安も有ります。

本業の方も来年前半は特に忙しくなりそうで、気が抜けない日々になりそうです。
それでもこの辺りを乗り切っていけばまだまだ希望はあるので、引き続きやっていけたらと思います。

【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の単体入力に変換しています。

マウスの検索の無効化について

Microsoft PowerToysでは、デフォルトでマウスの検索機能が有効になっているようでした。
これが有効だと、Ctrlを2回押した際に、全体が暗くなった上でマウスの周りだけ明るい表示になります。

当然ゲーム中に起きると非常に邪魔であるため、この機能が必要無い方は無効化することをお勧めします。
簡単なやり方は、マウス ユーティリティからマウスの検索を無効にするだけです。

後書き

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