Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ツール比較しながら語るO/RマッパーとDBマイグレーション
Search
Yu Watanabe
December 18, 2018
Technology
170
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
ツール比較しながら語るO/RマッパーとDBマイグレーション
Yu Watanabe
December 18, 2018
More Decks by Yu Watanabe
See All by Yu Watanabe
JUnitテストをCI環境で並列で実行する方法とその速度, スケーラビリティ
nabedge
6
2.9k
クラウド時代だからSpring-Retryフレームワーク
nabedge
0
310
JavaでWebサービスを作り続けるための戦略と戦術
nabedge
0
100
サーバーサイドな人がフロントエンド技術と仲良くするはじめの一歩
nabedge
0
81
Selenium再入門
nabedge
0
71
Webエンジニアがスタートダッシュをキメるためのローカル開発環境の勘所
nabedge
0
91
テストゼロからイチに進むための戦略と戦術
nabedge
0
100
jOOQってなんて読むの?から始めるSpringBootとO/Rマッパーの世界
nabedge
0
150
あなたのプロジェクトが気軽にJavaをバージョンアップするために必要なこと
nabedge
0
68
Other Decks in Technology
See All in Technology
AI-DLCを “そのまま導入しなかった”話 ~組織に合わせてアジャストした 私たちの実践共有~
hiroramos4
PRO
0
210
10年間のブログ発信を振り返って見えたWebアプリケーションエンジニアとしての軌跡
stefafafan
0
160
脆弱性対応、どこで線を引くか
rymiyamoto
1
420
小さく始める AI 活用推進 ― 日経電子版 Web チームの事例/nikkei-tech-talk47
nikkei_engineer_recruiting
0
300
Kiroで書いた 設計書 が AI レビューの 採点基準 になる
ezaki
0
130
Bucharest Tech Week 2026 - Guardians of the Cloud-Native Galaxy
edeandrea
PRO
0
120
コミュニティの有益性 ~JAWS Days 2026 での体験を通して~ / The Benefits of a Community ~Through My Experience at JAWS Days 2026~
seike460
PRO
0
180
【Snowflake Summit 2026 Recap!!】Snowflake Summit Deep Dive: Security & Governance
civitaspo
1
270
データレイクの「見えない問題」を可視化する
sansantech
PRO
1
100
【セミナー資料】Claude Code をセキュアに使うための考え方と設定の勘どころ / Claude Code Webinar 20260616
masahirokawahara
2
420
ぼっちではじめた登壇が「51名」「241件」の発信に化けた
subroh0508
1
240
2026TECHFRESH畢業分享會 - Lightning Talk - 打造精準高效的 MCP 設計模式與測試實務
line_developers_tw
PRO
0
1.3k
Featured
See All Featured
A better future with KSS
kneath
240
18k
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
The Spectacular Lies of Maps
axbom
PRO
1
820
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
Designing for Timeless Needs
cassininazir
1
260
Tips & Tricks on How to Get Your First Job In Tech
honzajavorek
1
540
RailsConf 2023
tenderlove
30
1.5k
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
160
Producing Creativity
orderedlist
PRO
348
40k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.5k
Transcript
#ccc_a1 ツール比較しながら語る O/RマッパーとDBマイグレーション JJUG-CCC 2018 Fall 日本Javaユーザーズグループ クロスコミュニティカンファレンス ベルサール西新宿 2018-12-15
Y.Watanabe
#ccc_a1 (ストップウォッチ スタート確認)
#ccc_a1 Who? • 渡辺 祐 • (株)ビズリーチ • SREグループ ◦
Site Reliability Engeneering • twitter: @nabedge •
[email protected]
3
#ccc_a1 Software Design 2019 / 1月号に寄稿しました 第2特集 リリースモデルの変更にどう対処する? Javaのバージョン問題に前向きに 取り組む方法
第3章 Javaをバージョンアップしやすくする アイデア 進化に臆さず,そのメリットを 享受するために 4
#ccc_a1 今日、伝えたいこと • アプリケーションの寿命 < DBの寿命 • 流れの速さのギャップに、アプリケーションのテク ノロジーで、うまいこと付き合う方法ってなんだろ う?
5
#ccc_a1 さっそくですがアンケート • MyBatis(iBatis) • SpringのJdbcTemplate • Hibernate • QueryDSL
• jOOQ • Doma • DBFlute • S2JDBC • Flyway 過去1年であなたが実際に仕事で使ったものは? 6
#ccc_a1 ただし! • 銀の弾丸は無い • エンジニアが現場の状況にあわせて ツールをチョイスして運用するしかない 7
#ccc_a1 もくじ1 1. タイプセーフなO/Rマッパーの特徴 2. DBマイグレーションツールとは? 3. Flywayとは? 8
#ccc_a1 もくじ2 4. 開発のスタート地点は? CREATE/ALTER文? ER図? テーブル定義書.xls ? JPAのエンティティクラス.java @Table,
@Column 5. 開発用DBはどこにある? ローカルPC? 共有DBサーバ? 9
#ccc_a1 もくじ3 6. O/Rマッパーのソースコード自動生成を どのタイミングでやるか 7. 自動生成したコードをgitに入れるか 8. 自動生成したコードとドメインオブジェ クトのコードを分けるべきか
10
#ccc_a1 もくじ4 9. テストデータをどうやって投入するか 10. 実際に実行されるSQLを見たい 11. RDBMSの独自関数を使いたい 12. テーブル定義書をどう作るか
11
#ccc_a1 もくじ5 13. 複数のO/Rマッパーを同じプロジェクトで使う or乗り換えるためのヒント 12
#ccc_a1 (盛り込み過ぎ...) 13
#ccc_a1 1. モダンなO/Rマッパーの特徴 14
#ccc_a1 • Hibernate 2001〜 • Spring-JDBC(JdbcTemplate) 2001〜 • iBatis/MyBatis 2005〜
• S2JDBC 2008〜 • QueryDSL 2008〜 • DBFlute 2008〜 • jOOQ 2010〜 テーブル作成済みのDBサー バからメタデータを読み取っ てO/Rマッピング用Javaソー スを自動生成する方式 古い順に並べて超ざっくり分類 SQLを手で埋め込む方式 素人にはおすすめできない(*1,2) 15
#ccc_a1 SpringのJdbcTemplate List<Book> books = jdbcTemplate.query( “SELECT ISBN, TITLE FROM
BOOKS” + “ WHERE ISBN = ? ”, new Object[]{“hoge”}, // “?”のところに入れたい引数 new BeanPropertyRowMapper(Book.class) ); 16
#ccc_a1 MyBatis <!-- xmlファイル --> <select id="selectBook" parameterType=”String” resultType="Book"> <![CDATA[
SELECT ISBN, TITLE FROM BOOKS WHERE ISBN = #{isbn} ]]></select> // Javaコード List<Book> books = bookRepository.select(“hoge”); 17
#ccc_a1 いま紹介したのは旧来型O/Rマッパー • メリット ◦ とにかくSQLを手で書かないと気が済まない人 • デメリット ◦ タイプセーフではない
◦ BOOKをBOOKSと書いても実行するまで(バグるまで)ミ スに気づけない ※想定しているテーブル名はBOOKです。前のページは わざと間違いを書いています。 18
#ccc_a1 jOOQ(ジュークと読む) //テーブルのメタデータ情報クラス Book book = Tables.book; // SQLを組み立てて実行 List<BookVo>
books = dsl .select(book.isbn, book.title) .from(book) .where(book.isbn.eq(“hoge”)) .fetchInto(BookVo.class); // PoJoであれば手作りクラスでも可 タイプセーフ=間違えたらコンパイルエラーでわかる 赤字は自動されたJavaコードを 使っている箇所 19
#ccc_a1 DBFlute List<Book>books = bookBhv.selectEntity( condition -> { condition.query().setBookIsbn_equal("999"); }
); 赤字は自動されたJavaコードを 使っている箇所 20
#ccc_a1 うたぐり深い人へ、本当の話。 1. jOOQはもっと複雑なSQLを組み立てることも可能です。 a. 参考文献 https://docs.google.com/presentation/d/1MvsMo38Bt-2h4b_ZDSSXNSgq_UuweXx9P0HmlbO y8k8 2. 正直に言うと、DBFluteは
group by をサポートしていません。 a. そういうことは「外出しSQL」で書く方向。 b. 外出しSQLの結果マッピングや呼び出しコードの自動生成をサポート。 21 割愛
#ccc_a1 QueryDSL • jOOQと似てる(ので、サンプルコードは省略) • NoSQLも積極的(Lucene拡張、MongoDB拡張) • 2016年9月、QueryDSLのメインなコミッターが 「やりきったから、別の仕事やるわ。」 と事実上の開発停滞宣言。*3
• jOOQ陣営「QueryDSLおつかれ。俺たちはまだやるぜ」宣言。*4 • 2018年5月 約2年ぶりのバージョンアップ 22
#ccc_a1 いま風なO/Rマッパーの共通項 • タイプセーフなJavaコーディングでCRUDを書く ◦ ミスったらコンパイルエラー • DBにピッタリ合わせたJavaコードでCRUDを書く ◦ DB変更の影響範囲がコンパイルエラーでわかる
• 上記を実現するために、 ◦ テーブル作成済みのDBサーバから自動的にメタデータを 読み取って、Javaソースコードを自動生成 23
#ccc_a1 2. DBマイグレーションツールとは? 24
#ccc_a1 ここで言うDBマイグレーションとは DBに対する変更=DDL文の適用=を管理するツール • O/Rマッパー同梱型 ◦ Ruby on Rails ◦
DBFlute ◦ Hibernate(?) • 専用ツール型 ◦ LiquiBase ◦ MyBatis Migration ◦ Flyway 25
#ccc_a1 「いまの状態のDB -> 変更のDDLをあてる -> 次の状態のDBになる」 1. 「次の状態のDB」のフルDDL(CREATE文)を手で作っておく 2. 「今の状態から変更するためのDDL」も手で作っておく
3. DBFluteの”save-previous”コマンドで今の状態のDBの定義情報を保存 4. 3と1を使ってDBFluteの”alter-check”コマンドで下記を検証できる 今の状態 + 変更のDDL = 次の状態 5. 4の結果を見たDBAは安心して「変更のDDL」を本番DBで実行 6. 開発者はDBFluteの”replace-schema”で手元の開発DBを再構築 26 DBFluteのマイグレーション機能
#ccc_a1 LiquiBase • 却下。 • 巨大なXMLファイルを手でメンテし続ける前提だから 27 割愛
#ccc_a1 MyBatis Migrations • 時間が無いので割愛。 • 考え方はFlywayとよく似ている 28 割愛
#ccc_a1 3. Flywayとは (これが近年の本命) 29
#ccc_a1 基本的な考え方 DBマイグレーションツールが無い世界で DBA担当がDBに向かってやる基本動作は 究極、これだけ。 1. DDL文を FooBar-0001.sql ファイルに書いて保存。 2.
順に、一度だけ、実行する。 30
#ccc_a1 DB担当者の基本動作を そのままソース管理&実行管理する ツールが Flyway だと思えばいい 31
#ccc_a1 32 src/main/resources/db/migration/ V1.1__foo_init.sql <- 去年のサービス開始のとき V1.2__hoge_alter.sql <- 先月の機能追加のとき V1.3__add_foobar.sql
<- 来週のための機能追加 1. DBに対する変更を.sqlファイルで積み重ねてゆく 2. flywayを実行 $ ./gradlew flywayMigrate
#ccc_a1 33 3. 管理テーブルに無いsqlファイルだけが実行対象となる > SELECT ... FROM SCHEMA_VERSION version
| script | success ---------+-----------------------+--------- 0.1 | << Flyway Baseline >> | true 1.1 | V1.1__foo_init.sql | true 1.2 | V1.2__hoge_alter.sql | true 1.3 <- このレコードは未だ無いのでV1.3__add_foobar.sqlが対象 4. sqlファイルの追加や変更がない状態でもう一度 flywayMigrate して も、全て実行済みでSCHEMA_VERSIONに記録されていれば、何も起きな い(べき等性)
#ccc_a1 補足 • 運用中のDBに、途中から導入することも可能 ◦ Flyway ▪ “flyway baseline” でググる
◦ DBFlute ▪ 詳しくはマニュアルを ▪ O/Rマッパーとして使わずとも、他のマイグレーション 支援コマンド群だけ使うことが可能 34
#ccc_a1 ちょっと休憩 35 1. 水を飲む 2. 時間を確認 20分くらい?
#ccc_a1 4. 開発のスタート地点はどこ? 36
#ccc_a1 37 A. 手書きのDDL(を積んでゆくだけ) 最初にCREATE TABLE、 運用しながら ALTER, CREATE/DROP INDEX,
CREATE/DROP TABLE... B. ER図をまず書く。(そこからDDL文を自動生成) C. JPAのエンティティクラスを手書きし、Hibernate-JPAでDDL文 を自動生成 D. テーブル定義書.xlsと手書きのDDLを同時に書き続ける
#ccc_a1 38 A. 手書きのDDLをテキスト形式で積み上げる この方法以外はすべて、なんだかんだで... • ツールのセットアップと使い方が難しい • 引き継ぎが難しくなる •
ツールが有償かつツールにロックインされる • バージョン管理システムとの相性が...
#ccc_a1 5. 開発DBサーバはどこにあるべき? 39
#ccc_a1 40 A. 共有DB方式 チームのエンジニア全員のPCからサーバ室の1台のDBサー バに接続 B. ローカルDB方式 それぞれのエンジニアのPCに自分専用の開発DBを構築
#ccc_a1 ローカルDB方式 = Docker時代のデファクト 41 $ docker run mysql:5.7 $
./gradlew flywayMigrate • 不要なカラムを削除したい • 不適切な名前のカラムを RENAMEしたい • 新しい機能のために新しい テーブルを追加したい • 並行して作業できる • ただしFlywayの場合はsqlファイルのバージョン番号 だけは衝突しないように話し合う $ docker run mysql:5.7 $ ./gradlew flywayMigrate
#ccc_a1 6. O/Rマッパーのソースコード自動生成を どのタイミングでやるか 7. 自動生成したコードをgitに入れるか 42
#ccc_a1 ローカルDB方式 + 自動生成型O/R + Flyway の場合 1. エンジニアはそれぞれやりたいDB変更をDDLで書く 書いたら手元PCで
./gradlew flywayMigrate (手元のDBが変更される) 2. エンジニアはそれぞれ手元でO/RマッパのJavaコード生成を実行 自動生成したJavaコードはコミット対象外!(理由は後述) 3. 2.に合わせてアプリのJavaコードも書く 4. 手元のPCでアプリを起動 -> 動作確認 5. プルリクを作る -> masterブランチにマージ (続く) 43
#ccc_a1 ※以下はエンジニアのPCではなくCIサーバが実施 6. 全てのソースコードツリーをチェックアウト 7. CIサーバ内部でDockerでローカルDBを起動 8. ./gradlew flywayMigrate (ローカルDBの再構成)
9. ./gradlew [O/RマッパのJavaコード自動生成コマンド] 10. ./gradlew build ->全てのコードがjar/warファイル化される 11. アプリをデプロイする前に ./gradlew flywayMigrate -DdbHost=... ※今度はDBの向き先をデプロイ先環境内のDBにしておく 12. jar/warをデプロイ 44
#ccc_a1 前頁のポイント • DB変更とアプリケーションコードの変更を 同じブランチ/プルリクエストで作業できる ◦ FlywayのマイグレーションSQLがバッティングしないように、変更内容 と適用順序をエンジニア間で要調整 • O/Rマッパーの自動生成Javaコードはgitコミットしない
• そのかわりに開発者のPCと CIサーバそれぞれで 必要なタイミングで自動生成を実行 45
#ccc_a1 O/Rマッパーの自動生成コードもコミットしたい場合 • マイグレーションSQL文のコミットと、 O/RマッパーのJavaコード自動生成の 実行&コミットを、同時にやるべき。 • ということは、5頁前のような並行作業だとコンフリクトを起こし やすくなる。 ◦
特に自動生成したJavaコード部分のコンフリクト • ということは、直列にしか作業できない(かもしれない) 46 割愛
#ccc_a1 DBFlute = 自動生成コードをコミットする前提 • 例:他のカラムから導出、計算した結果を入れるプロパティを、 自動生成したエンティティクラスに追加 • 例:共通のWHERE句を組み立て易くするために検索条件生 成クラスに自作のメソッドを追加
(正確には加筆用の継承クラスがあらかじめ自動生成される) 47
#ccc_a1 8. O/Rマッパーが自動生成したJavaコードと ドメインオブジェクトのコードを 分けるべきか? 注:DDDのそれというよりはDTOに近いかも 48
#ccc_a1 がぜん、分けるべき。 49 RDB Repository Logic O/Rマッパー 自動生成したentity クラス ドメインクラス
/DTO ドメインクラス /DTO Controller ドメインクラス /DTO • setter/getterで地 道に詰め替え • MapStruct, Dozer, etc 長寿 長寿に なりがち コロコロ変 わる
#ccc_a1 ドメインクラスと自動生成クラスの名前衝突に注意 テーブル名 BOOK O/Rマッパが自動生成したエンティティクラスやメタデータクラス名 Book.java 丹念に手作りしたいDDD的なドメインクラスの名前 Book.java 50 名前衝突
#ccc_a1 // jOOQでのカスタム例 public class FooPrefixGeneratorStrategy extends DefaultGeneratorStrategy { @Override
public String getJavaClassName(final Definition definition, final Mode mode) { String name = super.getJavaClassName(definition, mode); switch (mode) { case POJO: return name + "Vo"; // エンティティクラスは BookVo.javaになる case DEFAULT: return 'Foo' + name; // メタデータクラスは FooBook.javaになる } return name; } 51 (正確にはTablesクラスの内部クラス)
#ccc_a1 ちょっと休憩 52 1. 水を飲む 2. 時間を確認 35分くらい?
#ccc_a1 9. テストデータの投入方法は? 53
#ccc_a1 テストデータは必須。しかし.... 54 • 空っぽのテーブルでアプリケーションの動作確認はできない • テストデータは固定ではない。特に日付。 ◦ 「発売前の本」のつもりのデータが常に 2018-12-15
だったら?
#ccc_a1 DBFluteの場合 55 ‘replace-schema’コマンドが 1. 全てのテーブル、インデックスを DROP -> CREATE 2.
xls, tsv, csvファイルがあればテストデータとしてINSERT csvファイル上の “$sysdate.addDay(7)” は コマンド実行時刻の7日後の値がDBカラムに入る
#ccc_a1 他の方法 56 A. RDBMSのcsv, tsvのバルクロード機能 a. 日付の相対指定が難しい B. INSERT文を用意して実行
a. 大量の手書きINSERT文が今後のDB変更に耐えられるか? C. 上記A,Bのハイブリッド a. csvで入れて相対日付カラムはUPDATE文 D. FlywayのJava-Based Migration a. DB定義変更用PJとは別PJとしてテスデータ用PJを作っておく b. SQL文ではなくJavaコードを作っておく c. INSERT文よりは楽。日付の相対指定も可能。
#ccc_a1 Flyway公式マニュアルによると 57 出典 *10 src/main/java/db/migration/V1_2__Another_user.java src/main/resources/db/migration/V1_3__HogeHoge.sql ./gradlew flywayMigrate でファイル名順に実行される
ループして値を変えながらINSERTすればいい
#ccc_a1 10. O/Rマッパが作るSQLを見たい 58 - 手書きのSQL以外は信用しないタイプのエンジニアのため に -
#ccc_a1 DBFluteの場合 • デフォルトでこんな感じ ◦ SqlLogHandlerでさらに細かい制御も可能 59 出典 *5 結果データも出てる
呼び出し元クラス
#ccc_a1 jOOQのデバッグログ 60 出典 *6
#ccc_a1 O/Rマッパーを問わない方法もある • JDBCドライバの中継器として稼働しつつ 実行しようとしているSQLをログ出力 (正確にはプリペアドステートメントだけのことがほとんど) ◦ p6spy ◦ log4jdbc
61
#ccc_a1 11. RDBMSの独自関数を使いたい 62
#ccc_a1 DBFluteの場合 • sql_calc_found_rowsくらいならデ フォルト対応 • 外出しSQLならなんでも書ける • フォーマットは2-Way-SQL •
呼び出し側コード (WHERE句の調整等)も 自動生成 63 出典 *7
#ccc_a1 jOOQの場合 64 出典 *8,9
#ccc_a1 12. テーブル定義書をどう作るか 65
#ccc_a1 自動生成 一択 66
#ccc_a1 DBFluteの場合 67 ‘doc’コマンド一発
#ccc_a1 SchemaSpyの場合 • jarを直接実行、あるいはdocker run (*11) • ER図も自動生成 68
#ccc_a1 13. 同じプロジェクトで 複数のO/Rマッパーを同時に使う or乗り換えるためのヒント 69
#ccc_a1 • 複数のO/Rマッパを好きに混ぜて使って、 いいとこどりできたら幸せ。 • 一つのWeb/DBプロジェクトの開発で、 2つ以上のO/Rマッパーを混ぜて使うことは 無理?、危険? • トランザクション管理ェェ...
70
#ccc_a1 ※ Spring Frameworkを使っているとして 71
#ccc_a1 72 @Autowired OrderBhv orderBhv; // DBFlute @Autowired DSLContext dsl;
// jOOQ @Transactional public void order(String isbn, Long memberId) { // 本を購入するメソッド Order order = new Order(); order.setIsbn(isbn); order.setMemberId(memberId); orderBhv.insert(order); Book book = Tables.Book; dsl.update(book) .set(book.STOCK, book.STOCK.minus(1)) .where(book.ISBN.eq(isbn)) .execute(); } • DBFluteでINSERT • jOOQでUPDATE • 一つのトランザクション (BIGIN〜 COMMIT) で実行されていればOK DBFlute jOOQ
#ccc_a1 2つのO/Rマッパが使用する javax.sql.DataSource オブジェクトが 確実に同じ(インスタンス)であれば 正しくトランザクション管理できる。 73
#ccc_a1 @Bean public javax.sql.DataSource dataSource() { // コネクションプール機構を使うとして(ここではHikariCP) HikariConfig config
= new HikariConfig(); config.setJdbcUrl(...); config.setUsername(...); config.setPassword(...); HikariDataSource ds = new HikariDataSource(config); // return ds; // ←こうじゃなくて↓こう return new TransactionAwareDataSourceProxy(ds); } 74 詳しくは TransactionAwareDataSourceProxy でググる。
#ccc_a1 まとめ 75
#ccc_a1 Java/DB開発の今どきの手法とツール • O/Rマッピングライブラリ ◦ ソースコード自動生成によるタイプセーフ方式 ◦ 外部SQLファイル実行方式 • 実行したSQLのロギング
• DBマイグレーションの自動化 • テストデータ投入の自動化 • テーブル定義書の自動作成 • トランザクションに気をつけて複数のO/Rマッパーを同時に使用 76 選択肢と使い方をよく吟味して、レッツ快適開発!
#ccc_a1 Thank you ! 77
#ccc_a1 参考文献 1. Hibernateはどのようにして私のキャリアを破滅寸前にしたか https://www.kaitoy.xyz/2017/02/23/how-hibernate-ruined-my-career/ 上記の原文 https://medium.com/@ggajos/how-hibernate-almost-ruined-16f31ba7d381 2. 我々はいかにして技術選択を間違えたのか? https://blog.cybozu.io/entry/2016/12/28/101500
3. https://groups.google.com/forum/#!msg/querydsl/fNFXliG8P-k/7dy2aAotVQ0J 4. https://blog.jooq.org/2014/05/29/querydsl-vs-jooq-feature-completeness-vs-now-more-than-ever/ 5. http://dbflute.seasar.org/ja/manual/function/genbafit/implfit/debuglog/index.html 6. https://www.jooq.org/doc/3.11/manual/sql-execution/logging/ 7. http://dbflute.seasar.org/ja/manual/function/ormapper/outsidesql/howto.html 8. https://www.jooq.org/doc/3.11/manual/sql-execution/query-vs-resultquery/ 9. https://www.jooq.org/doc/3.11/manual/sql-building/plain-sql 10. https://flywaydb.org/documentation/migrations#java-based-migrations 11. https://hub.docker.com/r/schemaspy/schemaspy/ 12. 78