White Box技術部

WEB開発のあれこれ(と何か)

【MySQL 8向け】MyBatis Migrationsのコンテナ化

JVM系のマイグレーションツールの導入

新しくSpring Bootの開発環境をコンテナ上で作る上で、DBマイグレーションツールのMyBatis Migrationsをコンテナで使えるようしたので、その手順をまとめておきます。

f:id:seri_wb:20190605223604p:plain

Docker Composeの構成

作っていた開発環境のマイグレーション関連部分に絞ってディレクトリ構成を表すと、以下のようになっています。

$ tree -d
.
├── sample
│   └── migration
│       ├── drivers
│       └── sample
│           ├── environments
│           └── scripts
└── mysql
    ├── conf.d
    ├── data
    ├── log
    ├── sql
    └── tmp
        └── data

プロジェクトルートにdocker-compose.ymlを置き、それ起因でコンテナを運用するようにしています。

マイグレーション自体はdocker composeを持つリポジトリとは別で管理しているので、 アプリケーション名のディレクトリ(ここではsample)配下に、migrationディレクトリとしてgit cloneして利用するイメージです。

  • docker-compose.yml
version: '3.7'
services:
  migration:
    build:
      context: ./sample/migration/
    image: sample-migration
    container_name: sample_migration
    command: /bin/bash
    tty: true
    working_dir: "/sample/migration"
    volumes:
      - ./sample/migration:/sample/migration:cached
    depends_on:
      - db

  db:
    image: mysql:8
    container_name: sample_db
    environment:
      MYSQL_ROOT_PASSWORD: "root"
      #MYSQL_DATABASE: "sample"
      MYSQL_USER: "admin"
      MYSQL_PASSWORD: "admin"
    volumes:
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - ./mysql/conf.d:/etc/mysql/conf.d
      - ./mysql/data:/var/lib/mysql
      - ./mysql/tmp/data:/tmp/data
      - ./mysql/log:/var/log/mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"

このdocker-compose.ymlでは、MySQL 8のコンテナを立ち上げ、その後マイグレーション用のコンテナを立ち上げています。

MySQLのコンテナイメージ

MySQLのコンテナイメージでは、MYSQL_DATABASEを指定することで、予めDBを作成することもできるのですが、 DBは複数利用することを考え、コンテナイメージ作成時に実行されるinit.sqlで作成するようにしています。

CREATE DATABASE sample CHARACTER SET utf8mb4;
GRANT ALL ON *.* TO 'admin'@'%' WITH GRANT OPTION;
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'admin';
CREATE USER 'admin'@'127.0.0.1' IDENTIFIED BY 'admin';
FLUSH PRIVILEGES;
MyBatis Migrationsのコンテナイメージ

本題のMyBatis Migrationsのコンテナイメージは、OpenJDKのイメージをベースに作成します。

  • sample/migration/Dockerfile
FROM openjdk:11-slim

RUN apt-get update && apt-get install -y \
        wget \
        unzip \
    && apt-get clean \
    && rm -rf /var/lib/apt

RUN wget https://github.com/mybatis/migrations/releases/download/mybatis-migrations-3.3.5/mybatis-migrations-3.3.5-bundle.zip \
    && unzip mybatis-migrations-3.3.5-bundle.zip -d /usr/local \
    && rm mybatis-migrations-3.3.5-bundle.zip

ENV MYBATIS_MIGRATIONS_HOME /usr/local/mybatis-migrations-3.3.5
ENV PATH $PATH:$MYBATIS_MIGRATIONS_HOME/bin

WORKDIR /sample/migration

COPY . .
CMD ["/bin/bash"]

docker-compose up -d後(コンテナイメージ起動後)、コンテナに入ってマイグレーションスクリプトのテンプレートを作成します。

mkdir sample
cd sample
migrate init

この際、コマンド実行ディレクトリにdriversディレクトリが作成されますが、複数DBのスクリプト管理をすることを考え、WORKDIR配下にdriversを移動させます。

実際に利用するMySQLJDBCコネクタは、以下からダウンロードし、先ほどのディレクトリに配置してください。

その後、プロパティの以下を生成された値から変更します。

  • time_zoneをAsia/Tokyoに
  • script_char_setをアンコメント
  • JDBCに必要な設定を追加(driverのクラスに注意)
  • send_full_scriptをアンコメントしてfalseに
  • driver_pathをJDBCコネクタを配置したコンテナ内の絶対パス

  • sample/migration/sample/environments/development.properties

## Base time zone to ensure times are consistent across machines
time_zone=Asia/Tokyo

## The character set that scripts are encoded with
script_char_set=UTF-8

## JDBC connection properties.
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://db:3306/sample
username=admin
password=admin

#
# A NOTE ON STORED PROCEDURES AND DELIMITERS
#
# Stored procedures and functions commonly have nested delimiters
# that conflict with the schema migration parsing.  If you tend
# to use procs, functions, triggers or anything that could create
# this situation, then you may want to experiment with
# send_full_script=true (preferred), or if you can't use
# send_full_script, then you may have to resort to a full
# line delimiter such as "GO" or "/" or "!RUN!".
#
# Also play with the autocommit settings, as some drivers
# or databases don't support creating procs, functions or
# even tables in a transaction, and others require it.
#

# This ignores the line delimiters and
# simply sends the entire script at once.
# Use with JDBC drivers that can accept large
# blocks of delimited text at once.
send_full_script=false

# This controls how statements are delimited.
# By default statements are delimited by an
# end of line semicolon.  Some databases may
# (e.g. MS SQL Server) may require a full line
# delimiter such as GO.
# These are ignored if send_full_script is true.
delimiter=;
full_line_delimiter=false

# If set to true, each statement is isolated
# in its own transaction.  Otherwise the entire
# script is executed in one transaction.
# Few databases should need this set to true,
# but some do.
auto_commit=false

# If set to false, warnings from the database will interrupt migrations.
ignore_warnings=true

# Custom driver path to allow you to centralize your driver files
# Default requires the drivers to be in the drivers directory of your
# initialized migration directory (created with "migrate init")
driver_path=/sample/migration/drivers

# Name of the table that tracks changes to the database
changelog=CHANGELOG

# Migrations support variable substitutions in the form of ${variable}
# in the migration scripts.  All of the above properties will be ignored though,
# with the exception of changelog.
# Example: The following would be referenced in a migration file as ${ip_address}
# ip_address=192.168.0.1

これで準備完了です。

使い方

実際に利用する際は、initしたときのように起動後にコンテナ内に入ってmigrate upを実行でもいいですが、以下のように直接実行することもできます。

$ docker-compose up -d
$ docker-compose exec migration bash -c "cd sample && migrate up"

初回の起動スクリプトなどを組む場合は、こちらの使い方が便利です。

手順は以上です。よいコンテナライフを!

PythonでのAWS Lambda開発メモ(利用開始編)

Lambdaを使うことになった経緯

Lambdaの利用経験がなかった私が、Lambda利用を決めた流れは

  1. 1日1回のS3のファイルチェック処理が必要になる
  2. ファイルがなかったらSlack通知してくれればいい(簡単なスクリプトでOK
  3. 今のところ、AWS上にサーバは立ってない
  4. 調べてみたらLambdaもスケジュール実行できるらしい
  5. ならインフラコストを考えるとLambdaがいいか

でした。

Lambdaで利用する言語については、JupyterやGlueと合わせてPythonに。

LambdaやPythonの学習コストは、自分が払って展開すれば大したことにはならないだろうという見込みもあり、

「ついに私もサーバレスか・・・」

と感慨深く利用を始めました。

Lambda利用のために何をやったらいいのか?

Lambdaはマネージドサービスなので、AWS Console内で作業ができるようになっています。 エディタも用意されており、そこのテンプレートコードにあるlambda_handle関数に処理を書けば、指定のトリガーで実行される仕組みです。

言えば簡単なのですが、以下を懸念していました。

  • Pythonのライブラリ利用について
  • コード管理
  • ログ管理
  • テスト実行(テストコードではなく)
  • デプロイの自動化

なので、今回はこれらについて記事にしようと思います。


Pythonのライブラリ利用について

Lambdaを利用した際に、最初から用意されているライブラリはBoto3で、それ以外を利用したい場合は、利用するPythonのコードと一緒にzipでまとめて、アップロードする必要がありました。

今回はSlack通知の処理部分にrequestsを使いたかったので、zipでまとめてアップロードしています。

コード管理

そんな事もあって、幸か不幸かLambdaのコンソール上だけでは完結しなかったため、Lambdaプロジェクト用のGitHubリポジトリを作成し、コードは当該リポジトリ配下で記述するようにしました。

ディレクトリ構成はLambdaコンソールを参考に、リポジトリルート直下に関数名のディレクトリを作成して、そこにコードを置き、リポジトリルート直下には以下のようなシェルファイルを作成して、アップロード用のzipを作る、といった具合の構成です。

rm -rf upload.zip
pip3 install requests -t ./someLambdaFunction
cd someLambdaFunction
zip -r ../upload.zip *

しくじりポイント

zipは展開後が関数名のディレクトリになるのではなく、ソースコードの配置になっていないと駄目でした。

Lambdaコンソール上では、例えばsomeLambdaFunction/lambda_function.pyのような構成になっているので、てっきりsomeLambdaFunctionディレクトリもzipに含めるのかと思っていましたが、含めてしまうとsomeLambdaFunction/someLambdaFunction/lambda_function.pyのようになり、動作しなくなります。

環境変数の利用

WebhookのURLなどは環境変数を利用することで、コードから除去することができます。

ログ管理

ログについては標準出力に出しさえすれば、特に設定もなくCloudWatch Logsで見れました。

テスト実行(テストコードではなく)

関数のテスト実行はLambda上でテスト用のトリガーを作成して、それを実行すればできます。

処理内部で外部パラメータを利用しないのであれば、トリガーで渡すパラメータはデフォルトのままで問題ありません。

ローカルで開発・実行できるように準備もしたのですが、アクセスキーの発行を渋られて、結局Lambda上でのみ動作テストすることになってしまいました。。

デプロイの自動化

デプロイは少し曲者で、AWSのマネージドで完結させることはできなそうでした。

最初はCodeDeployでいけるかと思ったのですが、CodeDeployはデプロイされているLambdaのバージョンを切り替えることしかできないようでした。 CloudFormationを使えばいけるのかもしれませんが、ちょっとイメージしているのとは違うというか、CIと流れるように実行したかったといいますか。

lambda-uploaderを使えばできそうだったのですが、アクセスキーが・・・
AWS Lambda Pythonをlambda-uploaderでデプロイ | DevelopersIO

というわけで、これはまだ手動です。

Lambdaを使ってみて

「思っていたより簡単に利用できてびっくりした」というのが素直な感想です。

サーバの用意もいらないし、実行のコスト(金額的な意味で)もほとんどかからないしで、いいとこづくめだなと。 でもその恩恵に預かれるのはLambdaを知っている人だけなんだなぁと思うと、読みかけのサーバレス本をちゃんと読んで、もっと使いこなせるようになっておこうという気になりました。

知は力なりとは言ったものですね。

ログ出力指針の書き方

アプリケーションのログ出力指針を作ったときに、どんな事を考えていたのかを思い出しながら、 ログ出力指針について書いていきたいと思います。

ログ指針作成にあたって

そもそもなぜログが欲しいのか?何に利用するのか?

これに対する答えを置いておかないと、内容がぶれてしまうため、最初は目的を定義しました。

目的の作成

目的としてすぐ思いついたのは障害時対策のような守りの要素でしたが、 ビッグデータに取り込んで学習に使うというような攻めの要素にも使いたかったので、 目的には両方を明記しました。

目的を定めたあとは、どのタイミングでログが出力されていればいいのか、必要となる情報はなんなのか、解析しやすいフォーマットは・・・

のように思考が進んだのですが、作成した指針もどんなときにも利用できるというわけでもないだろうし、 無条件に鵜呑みにされると再度考察する機会がなくなると思い、この指針の対象とする範囲を定義しました。

適用範囲の設定

適用範囲を定めたことで、範囲外のログはどういう扱いにするか?という課題が出てきたのですが(例でいうとサーバログ)、 このときは「自分たちはここの範囲のログを整理することとして考えています。他は私たちが持ちますか?そちらで持ちますか?」というように 関連するチームに聞いて調整をしました。

もしサーバログも持つことになっていた場合は、ログ指針別で定義するのではなく、これを拡張していたと思います。

どんなときログを埋め込むのか?または埋め込まないのか?

あとはレビューでも客観的にチェックができるように、ログの埋め込むポイントを定義しました。

出力禁止項目も定義することで、出しては駄目な項目がないかを、このタイミングでチェックしたり話し合ったりしていました。

ログのフォーマットは?

フォーマットに関しては扱いやすさを考慮して設定しました。

解析時にはLTSVが便利だったので、ファイルにはLTSV。
開発中のコンソールは見れればいいということでTSVにしました。

細かい出力順などは利用するフレームワークに基本倣うようにしています。

出力項目については、出せるもの、できれば出したいものなど、とりあえず列挙し、適時選定していこうというスタイルを取りました。

ログの保持方法

ここらへんまできて、アプリケーションとしてどういうものになるのかを置かなければいけないと思っていました。 Fluentdで収集するのか、保持期間はどうするのか、アプリケーション起動にはコンテナを使うのか、起動するインフラ環境はオンプレなのかクラウドなのか、などなど。

このときはインフラ担当者と作業責務が分かれていたため、何をどっちが担当するのかを一つずつ確認しながら行う必要があり、けっこう大変でした。

そしてそろそろ面倒くさいなーと思い、まとめに入ったログ指針が以下のようなものです。


ログ出力の目的

以下の目的のため、ログ出力を行います。

  • 攻撃や事故の予兆を検知し、早期対策するため
  • 攻撃や事故の事後調査のため
  • アプリケーションの運用監査のため
  • 行動ログから、アプリケーション改善の指針を定めるため

ログ出力要件

ログ出力の要件を記載します。

本設計の範囲とするログの種類

本ログ出力指針は、アプリケーションが出力するログを対象とします。

サーバへのアクセスログは対象外としますが、アプリケーションからDBへのアクセス(CURD)は対象とします。

ログに記録するべき内容・イベント

ユーザが画面上で行うイベントと、認証・アカウント管理や重要情報へのアクセスに関するイベントを記録してください。 特に個人情報に対してのアクセスは必ず記録するようにお願いします。

またログはユーザIDやユニークIDなど、行動実施者とイベントが紐付けれるように記録してください。

認証・アカウント管理や重要情報へのアクセスに関するイベント
  • ログイン・ログアウト(失敗も含む)
  • アカウントロック
  • ユーザ登録・削除
  • パスワード変更
  • 重要情報の参照
  • 主だった操作(商品購入、送金、メール送信など)

出力禁止項目

以下の内容はログに出力しないようにしてください。

  • パスワード
  • OAuth関連のキー情報
  • 個人情報

ログ出力項目

ログには、4W1H(いつ、誰が、どこで、何を、どのように)に従って、以下の項目を出力します。

  • 処理日時
  • アクセス元情報(ユーザIDなど)
  • アクセス対象(URL、ページ名、スクリプトIDなど)
  • 操作内容(閲覧、変更、削除など)
  • 捜査対象(リソースID、カテゴリIDなど)
  • 操作結果(成功・失敗、処理件数など)

また、アプリケーションのクラッシュ情報、Stacktraceなどを出力します。

ログフォーマット

LTSVで出力します。

ただし、コンソールにはTSVで出力します。

出力項目
項目 キー オプション
ログの出力日時 date yyyy-MM-dd HH:mm:ss.SSS
スレッド thread
ログレベル level
ロガーの名前 logger
ユーザID user
セッションID session
アクセスURL uri
ログメッセージ message
サンプル
date:2018-10-20 19:09:42.216 thread:reactor-http-nio-3   level:DEBUG logger:jp.sample.api.search.repository.ItemSearchRepository message:アプリケーションで設定するログメッセージ

ログ出力先

アプリケーションのログは、/var/log/app配下にアプリケーション単位で出力します。

例えばproduct-name(アプリケーション名)の場合は、/var/log/app/product-nameに用途別でログが出力されます。

ログファイル一覧

ログファイル名 用途
application.log アプリケーションが出力するログ
application_error.log アプリケーションログのERRORレベルのみ
gc.log JavaアプリケーションのGCログ
java_error%p.log Javaアプリケーションが異常終了した場合のログ(プロセス単位)
heap/dump.log ヒープエラーのダンプ

アプリケーションログのローテーション

アプリケーションログは日単位でローテーションします。

ログの保存期間

アプリケーションサーバ上では最低1週間、最大1ヶ月保持します。 不要になったログ(過去ログ)の削除は、アプリケーション側では実施せず、サーバ側(logrotate)で実施します。

その他、CloudWatchにログを転送し、長期保存を行います。

ログレベルの利用基準

ログレベル 意味合い 用途
FATAL 回復不可能なエラー 利用しない
ERROR エラー 想定外の処理の発生(システムエラー)に関する情報を出力する。Exception情報がある場合は、Stacktraceも本レベルで出力する。
WARN 警告 システム上エラーではないが、通常発生し得ない(発生を期待していない)処理の発生を記録する。業務処理のエラーを記録する場合も、本レベルで出力する。
INFO 情報 各種イベントの発火・実行の事実を出力する(行動ログ)
DEBUG デバック用の情報 開発時に参考となる情報を出力する。パラメータ値を出力する場合は、基本的にDEBUGとする。
TRACE デバッグよりも詳細な動作トレース 利用しない
Stacktraceの出力タイミング

エラーが発生した場合のStacktraceは、発生箇所で都度出力するのではなく、 エラー処理を実際に行う終端で出力します。

参考

LTSVの活用

LTSVはTSVでもあるので、タブ区切りの場所さえわかれば、以下のようにcutでデータを取得することが出来ます。

$ tail -n 10 application.log | cut -f 5

LTSVはキーがついているので、grepで対象のキーを指定して取得することができ、

$ egrep -o "message:[^タブ文字]*" application.log

※タブ文字はCtrl-vを入力後、Tabキーを押下することで入力できます

以下のようなPerlワンライナーで意図したキーのみを抽出し、好きにフォーマットすることも出来ます。

$ cat application.log | perl -F'\t' -nale '%h=map{split/:/,$_,2}@F;print"$h{date}\t$h{thread}\t"'



終わりに

いかがでしょうか?未完成なところも目につくと思います。

ですがこういうものは作ってそのまま見られなくなるか、また日の目を浴びたときに古くなっているということでそのまま捨てられたりするものだと思うので、 完璧なものでなくても良いと個人的には思います。

ではなぜログ指針の話をしたかというと、このような指針を元に、 自分たちのチームはどう考えてシステムを作っていくのかの意識合わせを行い、観念を共有することがシステム開発では一番大事だと思っているからです。

何もなければ考えをすり合わせるのも難しいので、適時このように課題への解決案を形にしながら、チームでチームのベースを作っていくのがいいのではないでしょうか。

それに捨てられず、ずっとメンテナンスされて、新しいメンバーがチームを知るために使える資料になっている可能性もないわけではないですからね。

参考文献

  • 安全なWebアプリケーションの作り方
  • 仕事ではじめる機械学習

【kotlin】Coroutinesを使って、Spring WebFluxでJDBC処理を行う

ちょっと記事を寝かせすぎてしまったのですが、今回は1.3でKotlin本体に入ることが決まったコルーチンのお話です。


WebFluxでJDBCを使うには

以前の発表資料にも記載したのですが、

Spring Boot 2から使えるSpring WebFluxは、Spring MVCと違い、ノンブロッキングなWebフレームワークなので、 ブロッキング処理として作られているJDBCを使うには一工夫必要です。

Schedulers#elasticで対応

解決策としては、(この言い回しで合っているのかは自信がないのですが)JDBC処理をSchedulerのelasticでサブスクライブさせ、適時Workerに処理して貰うようにすることで、処理不整合を起こすことなく動かすことができます。

実際、プロダクトでWebFluxを使ったときはこの方法で実装し、アプリケーションに対しGatlingで負荷をかけても、この部分ではKOにならなかったため、期待した動作をしているようでした。

この方法の問題点

しかしこの方法で実装すると、Handlerで応答レスポンスを作るまでは、データをMonoかFluxで持ち回る必要が出てきます。

戻り値の型がMonoなどになっていると、メソッドの使い回しがしづらく、呼び出す際もブロッキングを考慮しないといけないため、そこそこの実装難易度となってしまい、結果、kotlinを使っていても理解しづらいコードになってしまいました。

これがどうにも気に入らずもやもやしていたのですが、JJUGのナイトセミナーでコルーチンの存在を知り、「コルーチンを使えば良い感じに書けるのでは?」と光明を見出し、コルーチンを使った実装を試してみました。

コルーチン

コルーチンは軽量スレッドとよく説明されますが、私はスレッド内でJavaScriptの非同期処理を実現する仕組みと理解しています。なのでブロッキング処理を非同期処理として実行するのにコルーチンが使えるのであれば、これで先ほどの問題を解決できるのではないかと考えた訳です。

その他の説明としては、ここの中断可能な計算インスタンスという説明がイメージしやすかったです
https://qiita.com/k-kagurazaka@github/items/8595ca60a5c8d31bbe37

コルーチンの導入

コルーチンはまだKotlin自体には組み込まれていないので、利用する場合はライブラリのkotlinx-coroutines-coreを追加する必要があります。

Gradleプロジェクトの場合は、以下のように追加します。

dependencies {
    compile('org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5')
    //...
}

WebFluxのリクエストをコルーチンで処理

例えば、リクエストでIDを受け取り、それをキーにRDBからデータを取得して、必要なデータを返すAPIをWebFluxとコルーチンで実装すると、以下のようになります。

package box.white.seriwb.api

import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.runBlocking
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.ServerResponse.ok
import reactor.core.publisher.Mono

@Component
class KotlinApiHandler {

    // ----- コルーチンのサンプル -----
    @Autowired
    lateinit var repository: KotlinApiRepository

    fun coroutineSelect(req: ServerRequest): Mono<ServerResponse> = runBlocking {

        val id = req.pathVariable("id").toLong()

        val responseData = async {
            repository.getSimpleResponseData(id)
        }

        ok().contentType(MediaType.APPLICATION_JSON_UTF8)
                .syncBody("{\"result\":\"${responseData.await().envelope}\"}")
    }
}

HandlerのcoroutineSelect関数は、JDBC処理(ブロッキング処理)がある以上、どこかで当該処理との待ち合わせ(ブロッキング)を行わないといけないのですが、 一方でこの関数全体がノンブロッキングである必要があります。 このため、Handlerの処理全体をコルーチンの処理として扱い、呼び出し元から見れば一つの処理となるようにしてあります。

具体的には、runBlockingで囲まれた部分がコルーチンで動作するので、

  1. Handler全体をrunBlockingで囲い、
  2. ブロッキング処理となるJDBCアクセスの処理は、asyncブロック内で呼び出し、
  3. その結果を利用するときにawaitメソッドを使うようにします。

今回はasyncが1つですが、2つ以上の場合は、asyncブロック内の処理結果が実際に必要になるところでawaitをまとめて呼び出すと、asyncの処理が並列で実行されます。

次はJDBC側の処理を見てみましょう。

Suspending関数を使う関数もSuspendingに

asyncブロック内で呼び出していたJDBC処理のgetSimpleResponseData関数は、Suspending関数とするため、suspend修飾子を付与して宣言します。

package box.white.seriwb.api

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

@Repository
class KotlinApiRepository {

    @Autowired
    lateinit var dao: KotlinApiDao

    @Transactional
    suspend fun getSimpleResponseData(id: Long): SimpleResponseData {
        val sample = dao.findSample(id)

        return SimpleResponseData(sample.value)
    }
}

data class SimpleResponseData(
    val envelope: String
)

どうしてSuspendingにするかというと、この処理は実際のJDBC処理となるfindSample関数を呼び出しているのですが、 このfindSample関数がSuspending関数だからです。 更に言うとSuspending関数を呼び出すのはSuspending関数内か、asyncブロック内である必要があるからです。

しかしSuspending関数にしたとはいえ、見て貰うとわかるように、 実際のJDBC処理となるDAOのfindSample関数を呼び出しているのにも関わらず、 suspend修飾子が付いている以外は普通の関数呼び出しの記述になっています。

つまりDAOを使うRepositoryの関数内では、Suspending関数も通常の関数呼び出しのように書けるのです。

ブロッキング処理はSuspending関数で

ではブロッキングが必要となる肝心要のDAOである、findSample関数はどうなるかというと、 こちらもsuspend修飾子を付けてSuspending関数にするだけで対応が終わります。

package box.white.seriwb.api

import box.white.seriwb.api.jooq.public_.Tables.SAMPLE
import box.white.seriwb.api.jooq.public_.tables.records.SampleRecord
import org.jooq.DSLContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository

@Repository
class KotlinApiDao {

    @Autowired
    lateinit var create: DSLContext

    suspend fun findSample(id: Long): SampleRecord {
        val result = create
                .selectFrom(SAMPLE)
                .where(SAMPLE.ID.eq(id))
                .fetch()
        return result.first()
    }
}

どうでしょうか、これであればKotlinのスッキリさを残したまま、WebFluxを利用できる気がしないでしょうか。

そしてコルーチンで利用する関数はSuspending関数として定義しているので、 通常の呼び出し方ができないことをコードとしても制約を課しているので、誤って呼び出されることを防止しています。

なんて便利なんでしょうか・・・

コルーチンとWebFluxでどうなったか

さて、これまでの話でコルーチンをWebFluxで使うことの利点を感じてもらえたかと思いますが、個人的に利点を上げると

ここらへんが大きなところかと思っています。

このようにコルーチンを利用するとWebFluxの勘所であるブロッキング処理を(そこそこ)簡単に扱えるので、 ここがWebFlux導入の障害となっていたのであれば、Kotlinとコルーチンを利用してみてはどうでしょうか?