jooq と flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

139
CyberAgent, Inc. All Rights Reserved JJUG CCC Fall 2015 #jjug_ccc #ccc_ab1 jOOQ と Flyway ととととととととととととととととと とと () Yusuke Ikeda / @yukung CyberAgent, Inc.

Upload: yusuke-ikeda

Post on 09-Jan-2017

10.475 views

Category:

Engineering


0 download

TRANSCRIPT

Page 1: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

JJUG CCC Fall 2015#jjug_ccc #ccc_ab1

jOOQ と Flyway で立ち向かう、自社サービスの保守運用(仮)

Yusuke Ikeda / @yukungCyberAgent, Inc.

Page 2: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

● 池田 裕介( @yukung )

● 株式会社サイバーエージェント 技術本部

● サーバサイドエンジニア

● Ameba プラットフォームの API 設計・運用

Java や Groovy のコミュニティによく出没します

About me

Page 3: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

https://www.cyberagent.co.jp/techinfo_detail/id=11016&season=2015&category=sever

Spring in Summer で発表しました

Page 4: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

突然ですが、質問です

Page 5: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

今、何かのサービスやシステムの

保守をしていますか?

Page 6: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

ソースコードのバージョン管理はしていますか?

Page 7: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

ユニットテストは書いていますか?

Page 8: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CI は回っていますか?

Page 9: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

データベースのスキーマも

バージョン管理していますか?

Page 10: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

本日のゴール

Page 11: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

本日のゴール

アプリだけでなく DB もセットで

バージョン管理+ CI して、

「つらくない」😁快適な運用保守ライフを送ろう!

Page 12: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

そう、

ならね。

Page 13: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

データベーススキーマのバージョン管理

データベーススキーマとアプリケーションコードとの乖離

保守フェーズで抱える悩み

Page 14: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

想定しているシチュエーション

運用・保守フェーズ

新規開発フェーズや、市場調査のための試行錯誤・技術検証フェーズでは

参考にならないかもしれません。

既存 の DB スキーマが存在

DB スキーマを開発者が設計に関わることができ、 DB スキーマの正規化が

有効になされているプロジェクトでは、必ずしも効果的ではないかもしれません。

DB スキーマとコードを効果的に管理できていない

フレームワークが提供している DB マイグレーションの仕組みなどを用いて、

既に DB マイグレーションを開発プロセスとして取り入れている人には、当たり前の話です。

Page 15: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Ameba プラットフォームについて

Page 16: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Ameba プラットフォームについて

Page 17: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

コミュニティ + ゲーム + メディ

アのプラットフォーム

2012 年 4 月ローンチ

現時点で延べ 300 サービス

会員数:約 3,900 万

月間 PV :約 144 億

月間投稿数:約 3,000 万

Ameba プラットフォーム

池田裕介
TODO Ameba プラットフォームの話は削る
Page 18: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Ameba のサービス提供フロー

ディベロッパー

App App App

2. 開発Ameba Developer Center 1. 登録

3. 申請

プラットフォームで提供するサービスの情報を集約・管理し、審査や公開を行う 5. 公開

認証システム

課金システム

分析クラスタ

ソーシャルグラフ API

Ameba プラットフォーム

4. データ連携

Page 19: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Ameba プラットフォームについて

Page 20: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Architecture

社内ログ解析基盤

Patriot

Database

developer.amebame.com

Mail Server

admin.developer.amebame.com

Backend service (Batch, etc)

Reverse Proxy

SPA + REST API

Page 21: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

いたって普通のサーバサイドアーキテクチャで

Page 22: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

Ameba プラットフォームについて

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Page 23: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

事案 1

Page 24: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

既存の仕様を…調べてたら

Page 25: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

よーしパパ MySQL 調べちゃうぞ〜

mysql> DESC hoge_table; っと…

あれっ!?このカラム、

ステージングにはあるけど

本番環境には無いぞ??

Page 26: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

しょうがない、

定義書の変更履歴見てみるか…

Page 27: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 28: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 29: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

事案 1

必要なのかどうか

分からない

ある項目が

いつ何のために

追加されたのか

わからない

影響が怖いので

触りたくない

→ → デッドコードが増える リファクタリングしにくい 保守性↓↓🙅

Page 30: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

事案 2

Page 31: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

あれっ… A さん、なんか定義書に無い

テーブルが存在してるんですが…

あぁ…それ定義書に反映してないんだ

と思うよー、最新にしといてー

Page 32: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

…めんどくさ…クッDDL から

リバースエンジニアリングするか…

→ 時間とともに秘伝のタレとなっていく 定義書 is 何

事案 2

Page 33: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

事案 3

Page 34: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

この SQL 、動くけど使ってない…?

コミットログ見てみるか…

池田裕介
TODO 事案3 は蛇足なので削る
Page 35: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 36: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 37: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

事案 4

Page 38: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

とある DAO クラスを

眺めていて

Page 39: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 40: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

これはつらい…

Page 41: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

何が『つらい』?

Page 42: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

もう一度

Page 43: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 44: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

事案 4

● 変更による影響が拾えない

● typo していても動かしてみないと気づかない

● JPA なら Criteria API 、 MyBatis なら Generator の

Criteria を使えばタイプセーフにはできるけど、抽象

度が上がってコードの可読性が下がる

つらいところ

文字列で記述されている

DB スキーマがコードに埋め込まれている

実際に動かさないとわからない

Page 45: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

MyBatis の Criteria の例

池田裕介
TODO 配布時には削る
Page 46: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

事案 4

● DB スキーマ変更の影響がエラーで検知できない

○ コード中に DB スキーマ情報が埋め込まれている

○ クエリが XML やプロパティファイルに記述して

あっても同じ

● 自動化されたユニットテストと CI 必須

つらいところ

文字列で記述されている

DB スキーマがコードに埋め込まれている

実際に動かさないとわからない

Page 47: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

事案 4

● SQL の文法エラーですら拾えない

○ 自動化されたユニットテストと CI 必須

○ 保守コストは高い

● DB スキーマの情報と、アプリケーションコードが地

続きになっていない

○ DB スキーマの変更も動かす前に検知したい

つらいところ

文字列で記述されている

DB スキーマがコードに埋め込まれている

実際に動かさないとわからない

Page 48: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマがコードに埋め込まれている

事案 4文字列で記述されている

実際に動かさないとわからない

つらくない開発を!もっと!

変更を「検知しやすく」、

可読性が高く「理解しやすい」状態

を保つ仕組みが欲しい

Page 49: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

Ameba プラットフォームについて

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Page 50: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの変更を追跡したい

アプリケーションコードだけでなく、 DB スキーマもバージョン管理する

スキーマの変更管理を楽にしたい

DB スキーマの変更管理を人手で行うのではなく、自動化して常に一貫性を保つ

変更検知を静的に、かつ

スキーマとコード の乖離を防ぐ

DB スキーマの変更によるアプリケーションコードへの影響を、動作させる前に検知

これらのワークフローを一気通貫に行いたい

解決したいこと

可読性を落とさずに、バージョン管理されたDB スキーマ情報をアプリケーションコードに活かす

Page 51: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの変更を追跡したい

スキーマの変更管理を楽にしたい

変更検知を静的に、かつ

スキーマとコード の乖離を防ぐ

解決したいこと

データベーススキーマのバージョン管理

データベーススキーマとアプリケーションコードとの乖離

Page 52: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの変更を追跡したい

スキーマの変更管理を楽にしたい

変更検知を静的に、かつ

スキーマとコード の乖離を防ぐ

解決したいこと

Flyway

jOOQ

Page 53: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

Ameba プラットフォームについて

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Page 54: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Flyway

Page 55: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Flyway とは

● DB マイグレーションツール

○ DB スキーマの構成管理を手軽に自動化できる

○ DDL や DML をマイグレーションスクリプトとして記述する

● Ant や Maven, Gradle などのビルドツールと一緒に利用するとよい

○ アプリケーションのビルドに DB スキーマの構成管理を統合でき

● CLI や Java API からでも使える

○ サーバサイドだけでなく Android の SQLite でも使える

● Git などのバージョン管理システムの管理下に置くことで、 DB スキー

マもコードで表現できる( Infrastructure as Code に似た考え方)

Page 56: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Flyway のセットアップ

Page 57: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Gradle (v2.1 以降 )

plugins { id "org.flywaydb.flyway" version "3.2.1"}flyway { url = "jdbc:mysql://host:port/sampledb" user = "sample_user" password = "any_password"}

Flyway のセットアップ

Page 58: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

create table PERSON ( ID int not null, NAME varchar(100) not null);

DDL をそのまま記述できる。

マイグレーションスクリプト

Page 59: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

デフォルトでは以下の命名規則にしたがって記述する。設定で変更することもできる。

スクリプトファイルの命名規則

V2__Add_new_table.sqlプレフィックス

バージョン

ドット (.) もしくはアンダースコア (_)で句切られた数字

セパレータ

アンダースコア 2 つ(__)

説明

バージョン管理用テーブルに記録される

サフィックス

Page 60: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

デフォルトでは以下のようなファイル配置とする。設定で変更することもできる。

スクリプトファイルの配置

プロジェクトルート

src

main

java

db

migration

resources

V0.2__Add_new_table.sql

Page 61: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Flyway でマイグレーション

Page 62: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

baseline コマンドを使う( init は flyway 4.0 で削除予定)$ ./gradlew flywayBaseline -i:flywayBaseline (Thread[Daemon worker Thread 2,5,main]) started.:flywayBaselineExecuting task ':flywayBaseline' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs.Flyway.init() is deprecated. Use baseline() instead. Will be removed in Flyway 4.0.Database: jdbc:h2:file:./build/test (H2 1.4)Creating Metadata table: "PUBLIC"."schema_version"Schema baselined with version: 1:flywayBaseline (Thread[Daemon worker Thread 2,5,main]) completed. Took 0.012 secs.

スキーマ管理テーブルの作成

Page 63: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

スキーマ管理テーブルの作成

マイグレーションの適用状態を管理する“ schema_version” というテーブルが作成される。

Page 64: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

info コマンドを使う。

$ ./gradlew flywayInfo -i:flywayInfo (Thread[Daemon worker,5,main]) started.:flywayInfoExecuting task ':flywayInfo' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs.Database: jdbc:h2:file:./build/test (H2 1.4)+---------+-----------------------+---------------------+---------+| Version | Description | Installed on | State |+---------+-----------------------+---------------------+---------+| 1 | << Flyway Baseline >> | 2015-11-25 14:39:19 | Baselin || 2 | Add new table | | Pending |+---------+-----------------------+---------------------+---------+

:flywayInfo (Thread[Daemon worker,5,main]) completed. Took 0.014 secs.

マイグレーションの状態を確認

State “が Pending”なのでまだ適用されていない

Page 65: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

migrate コマンドを使う。

$ ./gradlew flywayMigrate -i:flywayMigrate (Thread[Daemon worker Thread 2,5,main]) started.:flywayMigrateExecuting task ':flywayMigrate' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs.Database: jdbc:h2:file:./build/test (H2 1.4)Validated 2 migrations (execution time 00:00.003s)Current version of schema "PUBLIC": 1Migrating schema "PUBLIC" to version 2 - Add new tableSuccessfully applied 1 migration to schema "PUBLIC" (execution time 00:00.017s).:flywayMigrate (Thread[Daemon worker Thread 2,5,main]) completed. Took 0.045 secs.

マイグレーションする

version 2 のスクリプトが適用された

Page 66: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

マイグレーションスクリプトが適用され、バージョンが 1 つ進んだ

$ ./gradlew flywayInfo:flywayInfo+---------+-----------------------+---------------------+---------+| Version | Description | Installed on | State |+---------+-----------------------+---------------------+---------+| 1 | << Flyway Baseline >> | 2015-11-25 14:39:19 | Baselin || 2 | Add new table | 2015-11-25 14:46:56 | Success |+---------+-----------------------+---------------------+---------+

再度、状態を確認

State “が Success”になった

Page 67: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

再度、状態を確認

PERSON テーブルが作成されている。

Page 68: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB の現在の状態と、マイグレーションスクリプトの内容に一貫性があるかを検証する

create table if not exists PERSON ( ID integer primary key auto_increment, NAME varchar(100) not null);alter table PERSON add column AGE integer not null;

マイグレーションの状態を検証する

DB の状態と齟齬がある内容に変更

Page 69: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

validate コマンドを使い、マイグレーションスクリプトの内容を検証する$ ./gradlew flywayValidate:flywayValidate FAILED

FAILURE: Build failed with an exception.

* What went wrong:Execution failed for task ':flywayValidate'.> Error occurred while executing flywayValidate Validate failed. Migration Checksum mismatch for migration 2 -> Applied to database : 1401482110 -> Resolved locally : 1349563094

マイグレーションの状態を検証する

スクリプトがチェックサムエラーになる

Page 70: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

repair コマンドを使い、マイグレーション情報を削除してチェックサムを最新に更新$ ./gradlew flywayRepair -i:flywayRepair (Thread[Daemon worker Thread 3,5,main]) started.:flywayRepairExecuting task ':flywayRepair' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs.Database: jdbc:h2:file:./build/test (H2 1.4)Repair of failed migration in metadata table "PUBLIC"."schema_version" not necessary. No failed migration detected.Updating checksum of 2 to 1401482110 ...Metadata table "PUBLIC"."schema_version" successfully repaired (execution time 00:00.005s).Manual cleanup of the remaining effects the failed migration may still be required.:flywayRepair (Thread[Daemon worker Thread 3,5,main]) completed. Took 0.014 secs.

マイグレーションを修復する

チェックサムが更新され修復される

Page 71: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

clean コマンドを使うと、スキーマが全て削除される(やり直しはできないので、バックアップ必須)

$ ./gradlew flywayClean -i:flywayClean (Thread[Daemon worker Thread 4,5,main]) started.:flywayCleanExecuting task ':flywayClean' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs.Database: jdbc:h2:file:./build/test (H2 1.4)Cleaned schema "PUBLIC" (execution time 00:00.008s):flywayClean (Thread[Daemon worker Thread 4,5,main]) completed. Took 0.023 secs.

スキーマを削除する

Page 72: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Flyway の留意点

Page 73: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

巻き戻しはサポート外

● 既存データの取扱いや、参照整合性制約など考慮すべきことが多い

● 運用しているデータベースについては、スナップショットからリスト

アする方が安全

Page 74: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

複数環境や、並行開発への対応

● 特定のバージョンまで適用したい

○ target オプションを使ってバージョンを指定する

○ 運用環境でデプロイ時に指定するのは事故のもと

● ブランチを切って並行に開発している場合のバージョンの衝突

○ Flyway は適用されているバージョンより古いものは適用されない

○ outOfOrder オプションを true にすると、古いバージョンも適

用される

○ ファイル名を数字ではなくタイムスタンプにすると良い

■ V20151125114035__Add_age_column.sql

Page 75: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

Ameba プラットフォームについて

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Page 76: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

jOOQ

Page 77: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 78: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ とは

● SQL を Java コードで DSL で表現して DB アクセスできるライブラ

● JPA のように DB へのアクセスを抽象化するのではなく、できるだけ

SQL をそのままアプリケーションコードとして表現することにこだわ

● 裏側で何をやってるかを隠すのではなく、コード上から何をやってい

るかがわかるように

● “読み方は joke” らしい(海外の人の発音を聞くと「ジューク」の方が

近い?)○ https://groups.google.com/forum/#!msg/jooq-user/SGG7J5ulVBs/1l3XTtSVu9AJ

Page 79: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

百聞は一見にしかず

Page 80: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

著者の誕生日が 1920 年以降で、名前が Paulo さんの 書籍一覧を取得する SQL (本のタイトル昇順)

SELECT * FROM author a JOIN book b ON a.id = b.author_idWHERE a.year_of_birth > 1920 AND a.first_name = 'Paulo'ORDER BY b.title

サンプル

Page 81: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ …で同じクエリを記述すると

Result<Record> result =create.select() .from(AUTHOR.as("a")) .join(BOOK.as("b")).on(a.ID.equal(b.AUTHOR_ID)) .where(a.YEAR_OF_BIRTH.greaterThan(1920) .and(a.FIRST_NAME.equal("Paulo"))) .orderBy(b.TITLE) .fetch();

サンプル

Page 82: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

jOOQ の特徴

Page 83: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ の特徴

● Database First

○ 既存の DB スキーマを重視

■ レガシーなシステムの DB スキーマにも対応

○ 世の中、必ずしもキレイに正規化されたスキーマ

だけじゃないですよね?

○ 既存のふざけた DB スキーマに悪態つきつつも、

現実と戦うことは多いはず

この DB スキーマふざけてるな!!

誰だ設計したのは!!💢💢

レガシーDB

Page 84: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ の特徴

● Database First

○ 逆に言うと、以下の様な状況なら JPA の方が生産性は高いと思い

ます

■ 自分たちで DB スキーマをコントロールできる

■ きちんと正規化されているスキーマを相手にできる

○ クエリの自動生成機能などはなく、自分でガリガリクエリを書く必

要がある

■ 生産性の高さは謳っていない

■ 一応、 DAO クラスを生成ツールで自動生成できる

● 単純な CRUD ならそれを使うと実装しなくてもよい

Page 85: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

● Typesafe SQL

Result<Record> result =create.select() .from(AUTHOR.as("a")) .join(BOOK.as("b")).on(a.ID.equal(b.AUTHOR_ID)) .where(a.YEAR_OF_BIRTH.greaterThan(1920) .and(a.FIRST_NAME.equal("Paulo"))) .orderBy(b.TITLE) .fetch();

jOOQ の特徴 ● SQL キーワードをメソッドチェーンで繋ぐ

● テーブル名やカラム名、演算子も Java オブ

ジェクトやメソッドで表現

● 文字列をできるだけ使わない

● typo やスキーマの変更がコンパイルエラーとし

て検知できる

● IDE の補完がガシガシ利く

Page 86: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ の特徴

● Code Generation

○ DB スキーマのメタデータから Java オブジェク

トを生成(生成ツールが付属)

○ DB スキーマの変更は再生成することで反映

○ 一貫性がなければコンパイルエラーとして検知

○ DB スキーマとアプリケーションコードが地続き

Page 87: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ の特徴

● Active Records

○ 行は自動生成されたクラスのインスタンスにマッ

ピング

○ 自前の POJO にもそのままマッピングできる

Page 88: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ の特徴

● 他にも

○ Multi-Tenancy

■ 複数スキーマや共有スキーマに対応

○ Standardisation

■ RDBMS ごとの方言の差異を DSL で吸収

● …などなど

Page 89: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

jOOQ のいいところ

Page 90: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ のいいところ

● ドキュメントやチュートリアル、サンプルコードが

充実している

○ 公式ドキュメントがすごい(英語)

○ ビデオチュートリアルや各種フレームワーク/ラ

イブラリとの連携サンプルコードもたくさん

● Users Group や Stack Overflow も活発に回答が

● 商用ライセンスもある

Page 91: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 92: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 93: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 94: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ のいいところ

● 依存ライブラリが少ない

○ 標準では依存ライブラリなし

○ 設定により特定の機能を有効にすることで幾つか

のライブラリに依存する

Page 95: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

$ ./gradlew dependencies:dependencies------------------------------------------------------------Root project------------------------------------------------------------compile - Compile classpath for source set 'main'.\--- org.jooq:jooq:3.7.1

runtime - Runtime classpath for source set 'main'.\--- org.jooq:jooq:3.7.1

testCompile - Compile classpath for source set 'test'.\--- org.jooq:jooq:3.7.1

testRuntime - Runtime classpath for source set 'test'.\--- org.jooq:jooq:3.7.1

Page 96: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 97: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ のいいところ

● 後方互換性も大事にされており、活発に開発されてい

○ v3.7 で Java8 に正式対応

■ lambda 式 / Stream API

■ Optional

■ Date and Time API (JSR-310)

Page 98: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DSL.using(connection) .select( COLUMNS.TABLE_NAME, COLUMNS.COLUMN_NAME, COLUMNS.TYPE_NAME ) .from(COLUMNS) .where(COLUMNS.TABLE_NAME.equal("BOOK")) .orderBy( COLUMNS.TABLE_CATALOG, COLUMNS.TABLE_SCHEMA, COLUMNS.TABLE_NAME, COLUMNS.ORDINAL_POSITION ) .fetch() // ここまで jOOQ .stream() // ここから Stream API

.collect(groupingBy( r -> r.getValue(COLUMNS.TABLE_NAME), LinkedHashMap::new, mapping( r -> new Column( r.getValue(COLUMNS.COLUMN_NAME), r.getValue(COLUMNS.TYPE_NAME) ), toList() ) )) .forEach( (table, columns) -> { System.out.println( "CREATE TABLE " + table + " (" ); System.out.println( columns.stream() .map(col -> " " + col.name + " " + col.type) .collect(joining(",\n")) ); System.out.println(");"); } );

+----------+-----------+---------+|TABLE_NAME|COLUMN_NAME|TYPE_NAME|+----------+-----------+---------+|BOOK |ID |INTEGER ||BOOK |TITLE |VARCHAR |+----------+-----------+---------+

CREATE TABLE BOOK ( ID INTEGER, TITLE VARCHAR);

Page 99: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 100: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ のいいところ

● 何よりもタイプセーフで可読性が高いこと

○ 運用フェーズにおいて、コードの可読性は大事

■ 影響調査コストや保守性に直結する

○ 時間がしばらく経っても Repository 層( Dao

層)のコードを読めば何やってるかはだいたい分

かる

運用・保守フェーズの心強い味方😂

Page 101: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Getting started with jOOQ

Page 102: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

jOOQ ことはじめ

● クエリの組み立て● SQL 生成● 結果のフェッチ方法● レコードの更新

Page 103: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DSLContext (エントリポイント)

jOOQ では DSLContext というクラスで操作を行う

// 自動生成された DB スキーマオブジェクトを static インポート

import static org.yukung.sample.jooq.Tables.*;

DSLContext dsl = DSL.using(conn, SQLDialect.MYSQL);// AUTHOR は Tables クラスの中で定数定義されている

Result<Record> result = create.select().from(AUTHOR).fetch();

Page 104: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.select(field("book.title"), field("author.first_name"), field("author.last_name")) .from(table("book")) .join(table("author")) .on(field("book.author_id").equal(field("author.id"))) .where(field("book.published_in").equal(1915)) .fetch();

クエリの組み立て

Page 105: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

+----------+-----------------+----------------+|book.title|author.first_name|author.last_name|+----------+-----------------+----------------+|羅生門 |芥川 |龍之介 ||こころ |夏目 |漱石 |+----------+-----------------+----------------+

クエリの組み立て

Page 106: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .where(BOOK.PUBLISHED_IN.equal(1915)) .fetch();

クエリの組み立て

Page 107: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()) .fetch();

クエリの組み立て

Page 108: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

+-----+----------+---------+|title|first_name|last_name|+-----+----------+---------+|こころ |夏目 |漱石 ||羅生門 |芥川 |龍之介 |+-----+----------+---------+

クエリの組み立て

Page 109: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch();

クエリの組み立て

Page 110: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

String sql = dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .getSQL();

SQL 生成

Page 111: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

SELECT `jooq`.`book`.`title`, `jooq`.`author`.`first_name`, `jooq`.`author`.`last_name`FROM `jooq`.`book` JOIN `jooq`.`author` ON `jooq`.`book`.`author_id` = `jooq`.`author`.`id`ORDER BY `jooq`.`book`.`title` ASCLIMIT ? OFFSET ?

SQL 生成

Page 112: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch().formatCSV(',', "");

便利なフェッチ( CSV )

id,title,published_in,author_id1,羅生門 ,1915,1

Page 113: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch().formatJSON();

便利なフェッチ( JSON )

Page 114: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

{ "fields": [ { "schema": "jooq", "table": "book", "name": "id", "type": "INTEGER" }, { "schema": "jooq", "table": "book", "name": "title", "type": "VARCHAR" }, { "schema": "jooq", "table": "book", "name": "published_in", "type": "INTEGER" },

便利なフェッチ( JSON )

"records": [ [ 1, "羅生門 ", 1915, 1 ] ]}

Page 115: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch().formatXML();

便利なフェッチ( XML )

Page 116: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

<result xmlns="http://www.jooq.org/xsd/jooq-export-3.7.0.xsd"> <fields> <field schema="jooq" table="book" name="id" type="INTEGER"/> <field schema="jooq" table="book" name="title" type="VARCHAR"/> <field schema="jooq" table="book" name="published_in" type="INTEGER"/> <field schema="jooq" table="book" name="author_id" type="INTEGER"/> </fields> <records> <record> <value field="id">1</value> <value field="title">羅生門 </value> <value field="published_in">1915</value> <value field="author_id">1</value> </record> </records></result>

便利なフェッチ( XML )

Page 117: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch().formatHTML();

便利なフェッチ( HTML )

Page 118: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

<table> <thead> <tr> <th>id</th> <th>title</th> <th>published_in</th> <th>author_id</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>羅生門 </td> <td>1915</td> <td>1</td> </tr> </tbody></table>

便利なフェッチ( HTML )

Page 119: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

dsl.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetch().format();

便利なフェッチ( Text )

+----+-----+------------+---------+| id|title|published_in|author_id|+----+-----+------------+---------+| 1|羅生門 | 1915| 1|+----+-----+------------+---------+

Page 120: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

結果のフェッチ方法

自前の POJO クラスや Map にも自然にマッピングできる

List<BookDto> books = dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetchInto(BookDto.class);

Page 121: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

結果のフェッチ方法

自前の POJO クラスや Map にも自然にマッピングできる

Map<Integer, Book> books = dsl.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .from(BOOK) .join(AUTHOR) .on(BOOK.AUTHOR_ID.equal(AUTHOR.ID)) .where(BOOK.PUBLISHED_IN.equal(1915)) .orderBy(BOOK.TITLE.asc()).limit(3).offset(1) .fetchMap(BOOK.ID, Book.class);

Page 122: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

// 新しいレコードを作る

BookRecord book1 = dsl.newRecord(BOOK);book1.setTitle("JJUG CCC");book1.setPublishedIn(2015);book1.setAuthorId(2);// INSERT INTO book (title, published_in, author_id) VALUES ('JJUG CCC', 2015, 2);book1.store();

レコードの更新( Create )

Page 123: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

BookRecord book2 = dsl.fetchOne(BOOK, BOOK.ID.equal(3));book2.setTitle("JJUG CCC 2015");// UPDATE book SET title = 'JJUG CCC 2015' WHERE id = 3;book2.store();

BookRecord book = dsl.fetchOne(BOOK, BOOK.ID.equal(3));// DELETE FROM book WHERE id = 3;book.delete();

レコードの更新( Update / Delete )

Page 124: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Result<BookRecord> books = dsl.fetch(BOOK);modify(books);addSomething(books);

// バッチ更新 insert/updatedsl.batchStore(books);

バッチ更新

Page 125: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Contents

事例紹介

Ameba プラットフォームについて

実際に遭遇したこと

システム構成

解決したいこと

要素技術

ワークフロー

Flyway

jOOQ

Flyway と jOOQ の連携

Page 126: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

Flyway と jOOQ の連携

Page 127: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの変更を追跡したい

アプリケーションコードだけでなく、 DB スキーマもバージョン管理する

スキーマの変更管理を楽にしたい

DB スキーマの変更管理を人手で行うのではなく、自動化して常に一貫性を保つ

変更検知を静的に、かつ

スキーマとコード の乖離を防ぐ

DB スキーマの変更によるアプリケーションコードへの影響を、動作させる前に検知

これらのワークフローを一気通貫に行いたい

解決したいこと(再掲)

可読性を落とさずに、バージョン管理されたDB スキーマ情報をアプリケーションコードに活かす

Page 128: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

ワークフローの理想

DB スキーママイグレーション

スクリプト

DB マイグレーション

Git リポジトリ schema_version

+---------+-----------------------+---------------------+---------+| Version | Description | Installed on | State |+---------+-----------------------+---------------------+---------+| 1 | << Flyway Baseline >> | 2015-11-25 14:39:19 | Baselin || 2 | Add new table | 2015-11-25 14:46:56 | Success |+---------+-----------------------+---------------------+---------+

bookauthor

SchemaImpl

Tables

BOOK

BookRecord

AUTHOR

AuthorRecord

jOOQ コード

コード生成

成果物 jar

ビルド

この流れをビルドフローに組み込む

Page 129: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

ビルドシステムのサポート

● Flyway○ CLI (Java)○ Java API○ Ant○ Maven○ Gradle○ sbt

● jOOQ○ CLI (Java)○ Java API○ Ant / Gradle

■ Generation Tool○ Maven

■ Plugin

Page 130: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

Gradle のタスクグラフをカスタマイズする

ビルドワークフロー( Gradle )

Clean flywayMigrate

jooqGenerate

compileJava

processResources

classes

jar

assemble

build

flyway-gradle-pluginで追加されるタスク

自前で作成したタスク

task jooqGenerate(dependsOn: 'flywayMigrate') { // configuration for jOOQ}compileJava.dependsOn jooqGenerateprocessResources.dependsOn jooqGenerate

Page 131: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの一気通貫な CI

● 以下をトリガーとして前項のビルドフローを実行することで、 DB スキーマとアプリケーションの整合性

を CI でチェックできる○ ステージング・本番環境へのビルド・デプロイ○ IntegrationTest や FunctionalTest (E2E Test)

+ ++

Page 132: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

まとめ

Page 133: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

DB スキーマの変更を追跡したい

アプリケーションコードだけでなく、 DB スキーマもバージョン管理する

スキーマの変更管理を楽にしたい

DB スキーマの変更管理を人手で行うのではなく、自動化して常に一貫性を保つ

変更検知を静的に、かつ

スキーマとコード の乖離を防ぐ

DB スキーマの変更によるアプリケーションコードへの影響を、動作させる前に検知

Flyway と jOOQ で、一貫性と保守性を保ちながら、現実と向き合いつつも快適な運用保守ライフを!

まとめ

可読性を落とさずに、バージョン管理された DB スキーマをアプリケーションコードに活かす

Page 134: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

CyberAgent, Inc. All Rights Reserved

さいごに宣伝

Page 135: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1
Page 136: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

日本語翻訳しています(非公式だけどね)

Page 137: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

近日中に公開(できれば)したいです

頑張ります ( ˘ω˘)

Page 138: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

手伝ってくれる方歓迎! 懇親会でも Twitter でもお気軽に!

Page 139: jOOQ と Flyway で立ち向かう、自社サービスの保守運用 #jjug_ccc #ccc_ab1

ご清聴ありがとうございました