ツール比較しながら語るO/RマッパーとDBマイグレーション

6.7K Views

December 25, 18

スライド概要

JJUG-CCC 2018 Fall

profile-image

Engineer. Java, Kotlin(Server Side), JavaScript, Vue.js, Spring boot, CI/CD tool, build tool, monitoring, and activity as SRE

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

#ccc_a1 ツール比較しながら語る O/RマッパーとDBマイグレーション JJUG-CCC 2018 Fall 日本Javaユーザーズグループ クロスコミュニティカンファレンス ベルサール西新宿 2018-12-15 Y.Watanabe

2.

#ccc_a1 (ストップウォッチ スタート確認)

3.

#ccc_a1 Who? ● 渡辺 祐 ● (株)ビズリーチ ● SREグループ ○ Site Reliability Engeneering ● twitter: @nabedge ● nabedge@gmail.com 3

4.

#ccc_a1 Software Design 2019 / 1月号に寄稿しました 第2特集 リリースモデルの変更にどう対処する? Javaのバージョン問題に前向きに 取り組む方法 第3章 Javaをバージョンアップしやすくする アイデア 進化に臆さず,そのメリットを 享受するために 4

5.

今日、伝えたいこと #ccc_a1 ● アプリケーションの寿命 < DBの寿命 ● 流れの速さのギャップに、アプリケーションのテク ノロジーで、うまいこと付き合う方法ってなんだろ う? 5

6.

#ccc_a1 さっそくですがアンケート 過去1年であなたが実際に仕事で使ったものは? ● ● ● ● MyBatis(iBatis) SpringのJdbcTemplate Hibernate QueryDSL ● ● ● ● ● jOOQ Doma DBFlute S2JDBC Flyway 6

7.

ただし! ● 銀の弾丸は無い ● エンジニアが現場の状況にあわせて ツールをチョイスして運用するしかない #ccc_a1 7

8.

もくじ1 1. タイプセーフなO/Rマッパーの特徴 2. DBマイグレーションツールとは? 3. Flywayとは? #ccc_a1 8

9.

もくじ2 #ccc_a1 4. 開発のスタート地点は? CREATE/ALTER文? ER図? テーブル定義書.xls ? JPAのエンティティクラス.java @Table, @Column 5. 開発用DBはどこにある? ローカルPC? 共有DBサーバ? 9

10.

もくじ3 #ccc_a1 6. O/Rマッパーのソースコード自動生成を どのタイミングでやるか 7. 自動生成したコードをgitに入れるか 8. 自動生成したコードとドメインオブジェ クトのコードを分けるべきか 10

11.

もくじ4 9. テストデータをどうやって投入するか 10. 実際に実行されるSQLを見たい 11. RDBMSの独自関数を使いたい 12. テーブル定義書をどう作るか #ccc_a1 11

12.

もくじ5 #ccc_a1 13. 複数のO/Rマッパーを同じプロジェクトで使う or乗り換えるためのヒント 12

13.

#ccc_a1 (盛り込み過ぎ...) 13

14.

#ccc_a1 1. モダンなO/Rマッパーの特徴 14

15.

古い順に並べて超ざっくり分類 ● ● ● ● ● ● ● #ccc_a1 Hibernate 2001〜 素人にはおすすめできない(*1,2) Spring-JDBC(JdbcTemplate) 2001〜 iBatis/MyBatis 2005〜 SQLを手で埋め込む方式 S2JDBC 2008〜 テーブル作成済みのDBサー QueryDSL 2008〜 バからメタデータを読み取っ てO/Rマッピング用Javaソー DBFlute 2008〜 スを自動生成する方式 jOOQ 2010〜 15

16.
[beta]
#ccc_a1

SpringのJdbcTemplate
List<Book> books = jdbcTemplate.query(
“SELECT ISBN, TITLE FROM BOOKS”
+ “ WHERE ISBN = ? ”,
new Object[]{“hoge”}, // “?”のところに入れたい引数
new BeanPropertyRowMapper(Book.class)
);

16

17.
[beta]
#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

18.

いま紹介したのは旧来型O/Rマッパー #ccc_a1 ● メリット ○ とにかくSQLを手で書かないと気が済まない人 ● デメリット ○ タイプセーフではない ○ BOOKをBOOKSと書いても実行するまで(バグるまで)ミ スに気づけない ※想定しているテーブル名はBOOKです。前のページは わざと間違いを書いています。 18

19.

#ccc_a1 jOOQ(ジュークと読む) タイプセーフ=間違えたらコンパイルエラーでわかる //テーブルのメタデータ情報クラス Book book = Tables.book; 赤字は自動されたJavaコードを // SQLを組み立てて実行 使っている箇所 List<BookVo> books = dsl .select(book.isbn, book.title) .from(book) .where(book.isbn.eq(“hoge”)) .fetchInto(BookVo.class); // PoJoであれば手作りクラスでも可 19

20.
[beta]
#ccc_a1

DBFlute
List<Book>books = bookBhv.selectEntity(
condition -> {
condition.query().setBookIsbn_equal("999");
}
);
赤字は自動されたJavaコードを
使っている箇所

20

21.

うたぐり深い人へ、本当の話。 割愛 #ccc_a1 21 1. jOOQはもっと複雑なSQLを組み立てることも可能です。 a. 参考文献 https://docs.google.com/presentation/d/1MvsMo38Bt-2h4b_ZDSSXNSgq_UuweXx9P0HmlbO y8k8 2. 正直に言うと、DBFluteは group by をサポートしていません。 a. そういうことは「外出しSQL」で書く方向。 b. 外出しSQLの結果マッピングや呼び出しコードの自動生成をサポート。

22.

#ccc_a1 QueryDSL ● jOOQと似てる(ので、サンプルコードは省略) ● NoSQLも積極的(Lucene拡張、MongoDB拡張) ● 2016年9月、QueryDSLのメインなコミッターが 「やりきったから、別の仕事やるわ。」 と事実上の開発停滞宣言。*3 ● jOOQ陣営「QueryDSLおつかれ。俺たちはまだやるぜ」宣言。*4 ● 2018年5月 約2年ぶりのバージョンアップ 22

23.

いま風なO/Rマッパーの共通項 #ccc_a1 ● タイプセーフなJavaコーディングでCRUDを書く ○ ミスったらコンパイルエラー ● DBにピッタリ合わせたJavaコードでCRUDを書く ○ DB変更の影響範囲がコンパイルエラーでわかる ● 上記を実現するために、 ○ テーブル作成済みのDBサーバから自動的にメタデータを 読み取って、Javaソースコードを自動生成 23

24.

#ccc_a1 2. DBマイグレーションツールとは? 24

25.

ここで言うDBマイグレーションとは DBに対する変更=DDL文の適用=を管理するツール ● O/Rマッパー同梱型 ○ ○ ○ Ruby on Rails DBFlute Hibernate(?) ● 専用ツール型 ○ ○ ○ LiquiBase MyBatis Migration Flyway #ccc_a1 25

26.

#ccc_a1 DBFluteのマイグレーション機能 「いまの状態のDB -> 変更のDDLをあてる -> 次の状態のDBになる」 1. 2. 3. 4. 「次の状態のDB」のフルDDL(CREATE文)を手で作っておく 「今の状態から変更するためのDDL」も手で作っておく DBFluteの”save-previous”コマンドで今の状態のDBの定義情報を保存 3と1を使ってDBFluteの”alter-check”コマンドで下記を検証できる 今の状態 + 変更のDDL = 次の状態 5. 4の結果を見たDBAは安心して「変更のDDL」を本番DBで実行 6. 開発者はDBFluteの”replace-schema”で手元の開発DBを再構築 26

27.

LiquiBase 割愛 #ccc_a1 ● 却下。 ● 巨大なXMLファイルを手でメンテし続ける前提だから 27

28.

MyBatis Migrations ● 時間が無いので割愛。 ● 考え方はFlywayとよく似ている 割愛 #ccc_a1 28

29.

#ccc_a1 3. Flywayとは (これが近年の本命) 29

30.

基本的な考え方 #ccc_a1 DBマイグレーションツールが無い世界で DBA担当がDBに向かってやる基本動作は 究極、これだけ。 1. DDL文を FooBar-0001.sql ファイルに書いて保存。 2. 順に、一度だけ、実行する。 30

31.

#ccc_a1 DB担当者の基本動作を そのままソース管理&実行管理する ツールが Flyway だと思えばいい 31

32.

#ccc_a1 1. DBに対する変更を.sqlファイルで積み重ねてゆく src/main/resources/db/migration/ V1.1__foo_init.sql <- 去年のサービス開始のとき V1.2__hoge_alter.sql <- 先月の機能追加のとき V1.3__add_foobar.sql <- 来週のための機能追加 2. flywayを実行 $ ./gradlew flywayMigrate 32

33.

#ccc_a1 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に記録されていれば、何も起きな い(べき等性) 33

34.

#ccc_a1 補足 ● 運用中のDBに、途中から導入することも可能 ○ Flyway ■ “flyway baseline” でググる ○ DBFlute ■ 詳しくはマニュアルを ■ O/Rマッパーとして使わずとも、他のマイグレーション 支援コマンド群だけ使うことが可能 34

35.

#ccc_a1 ちょっと休憩 1. 水を飲む 2. 時間を確認 20分くらい? 35

36.

#ccc_a1 4. 開発のスタート地点はどこ? 36

37.

#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を同時に書き続ける

38.

#ccc_a1 A. 手書きのDDLをテキスト形式で積み上げる この方法以外はすべて、なんだかんだで... ● ツールのセットアップと使い方が難しい ● 引き継ぎが難しくなる ● ツールが有償かつツールにロックインされる ● バージョン管理システムとの相性が... 38

39.

#ccc_a1 5. 開発DBサーバはどこにあるべき? 39

40.

#ccc_a1 A. 共有DB方式 チームのエンジニア全員のPCからサーバ室の1台のDBサー バに接続 B. ローカルDB方式 それぞれのエンジニアのPCに自分専用の開発DBを構築 40

41.

ローカルDB方式 = Docker時代のデファクト $ docker run mysql:5.7 $ ./gradlew flywayMigrate ● 不要なカラムを削除したい ● 不適切な名前のカラムを RENAMEしたい #ccc_a1 $ docker run mysql:5.7 $ ./gradlew flywayMigrate ● 新しい機能のために新しい テーブルを追加したい ● 並行して作業できる ● ただしFlywayの場合はsqlファイルのバージョン番号 だけは衝突しないように話し合う 41

42.

#ccc_a1 6. O/Rマッパーのソースコード自動生成を どのタイミングでやるか 7. 自動生成したコードをgitに入れるか 42

43.

#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

44.

#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

45.

前頁のポイント #ccc_a1 45 ● DB変更とアプリケーションコードの変更を 同じブランチ/プルリクエストで作業できる ○ FlywayのマイグレーションSQLがバッティングしないように、変更内容 と適用順序をエンジニア間で要調整 ● O/Rマッパーの自動生成Javaコードはgitコミットしない ● そのかわりに開発者のPCと CIサーバそれぞれで 必要なタイミングで自動生成を実行

46.

#ccc_a1 O/Rマッパーの自動生成コードもコミットしたい場合 ● マイグレーションSQL文のコミットと、 46 割愛 O/RマッパーのJavaコード自動生成の 実行&コミットを、同時にやるべき。 ● ということは、5頁前のような並行作業だとコンフリクトを起こし やすくなる。 ○ 特に自動生成したJavaコード部分のコンフリクト ● ということは、直列にしか作業できない(かもしれない)

47.

DBFlute = 自動生成コードをコミットする前提 #ccc_a1 47 ● 例:他のカラムから導出、計算した結果を入れるプロパティを、 自動生成したエンティティクラスに追加 ● 例:共通のWHERE句を組み立て易くするために検索条件生 成クラスに自作のメソッドを追加 (正確には加筆用の継承クラスがあらかじめ自動生成される)

48.

#ccc_a1 8. O/Rマッパーが自動生成したJavaコードと ドメインオブジェクトのコードを 分けるべきか? 注:DDDのそれというよりはDTOに近いかも 48

49.

#ccc_a1 がぜん、分けるべき。 長寿 長寿に なりがち 49 RDB O/Rマッパー Repository コロコロ変 Logic わる Controller 自動生成したentity クラス ドメインクラス /DTO ドメインクラス /DTO ドメインクラス /DTO ● setter/getterで地 道に詰め替え ● MapStruct, Dozer, etc

50.

#ccc_a1 ドメインクラスと自動生成クラスの名前衝突に注意 テーブル名 BOOK O/Rマッパが自動生成したエンティティクラスやメタデータクラス名 Book.java 丹念に手作りしたいDDD的なドメインクラスの名前 Book.java 名前衝突 50

51.
[beta]
#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になる
}
(正確にはTablesクラスの内部クラス)
return name;
}

51

52.

ちょっと休憩 1. 水を飲む 2. 時間を確認 35分くらい? #ccc_a1 52

53.

#ccc_a1 9. テストデータの投入方法は? 53

54.

テストデータは必須。しかし.... #ccc_a1 ● 空っぽのテーブルでアプリケーションの動作確認はできない ● テストデータは固定ではない。特に日付。 ○ 「発売前の本」のつもりのデータが常に 2018-12-15 だったら? 54

55.

#ccc_a1 DBFluteの場合 ‘replace-schema’コマンドが 1. 全てのテーブル、インデックスを DROP -> CREATE 2. xls, tsv, csvファイルがあればテストデータとしてINSERT csvファイル上の “$sysdate.addDay(7)” は コマンド実行時刻の7日後の値がDBカラムに入る 55

56.

他の方法 #ccc_a1 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文よりは楽。日付の相対指定も可能。 56

57.

#ccc_a1 Flyway公式マニュアルによると ループして値を変えながらINSERTすればいい src/main/java/db/migration/V1_2__Another_user.java src/main/resources/db/migration/V1_3__HogeHoge.sql ./gradlew flywayMigrate でファイル名順に実行される 57 出典 *10

58.

#ccc_a1 10. O/Rマッパが作るSQLを見たい - 手書きのSQL以外は信用しないタイプのエンジニアのため に- 58

59.

#ccc_a1 DBFluteの場合 ● デフォルトでこんな感じ ○ SqlLogHandlerでさらに細かい制御も可能 呼び出し元クラス 出典 *5 結果データも出てる 59

60.

#ccc_a1 jOOQのデバッグログ 出典 *6 60

61.

#ccc_a1 O/Rマッパーを問わない方法もある ● JDBCドライバの中継器として稼働しつつ 実行しようとしているSQLをログ出力 (正確にはプリペアドステートメントだけのことがほとんど) ○ p6spy ○ log4jdbc 61

62.

#ccc_a1 11. RDBMSの独自関数を使いたい 62

63.

#ccc_a1 DBFluteの場合 63 ● sql_calc_found_rowsくらいならデ フォルト対応 ● 外出しSQLならなんでも書ける ● フォーマットは2-Way-SQL ● 呼び出し側コード (WHERE句の調整等)も 自動生成 出典 *7

64.

#ccc_a1 jOOQの場合 出典 *8,9 64

65.

#ccc_a1 12. テーブル定義書をどう作るか 65

66.

#ccc_a1 自動生成 一択 66

67.

#ccc_a1 DBFluteの場合 ‘doc’コマンド一発 67

68.

#ccc_a1 SchemaSpyの場合 ● ● jarを直接実行、あるいはdocker run (*11) ER図も自動生成 68

69.

#ccc_a1 13. 同じプロジェクトで 複数のO/Rマッパーを同時に使う or乗り換えるためのヒント 69

70.

#ccc_a1 ● 複数のO/Rマッパを好きに混ぜて使って、 いいとこどりできたら幸せ。 ● 一つのWeb/DBプロジェクトの開発で、 2つ以上のO/Rマッパーを混ぜて使うことは 無理?、危険? ● トランザクション管理ェェ... 70

71.

#ccc_a1 ※ Spring Frameworkを使っているとして 71

72.

#ccc_a1 72 @Autowired OrderBhv orderBhv; // DBFlute @Autowired DSLContext dsl; // jOOQ @Transactional public void order(String isbn, Long memberId) { // 本を購入するメソッド Order order = new Order(); DBFlute order.setIsbn(isbn); ● DBFluteでINSERT order.setMemberId(memberId); ● jOOQでUPDATE orderBhv.insert(order); ● 一つのトランザクション Book book = Tables.Book; (BIGIN〜 COMMIT) dsl.update(book) で実行されていればOK .set(book.STOCK, book.STOCK.minus(1)) .where(book.ISBN.eq(isbn)) jOOQ .execute(); }

73.

#ccc_a1 2つのO/Rマッパが使用する javax.sql.DataSource オブジェクトが 確実に同じ(インスタンス)であれば 正しくトランザクション管理できる。 73

74.

@Bean public javax.sql.DataSource dataSource() { #ccc_a1 // コネクションプール機構を使うとして(ここではHikariCP) HikariConfig config = new HikariConfig(); config.setJdbcUrl(...); config.setUsername(...); config.setPassword(...); HikariDataSource ds = new HikariDataSource(config); // return ds; // ←こうじゃなくて↓こう return new TransactionAwareDataSourceProxy(ds); } 詳しくは TransactionAwareDataSourceProxy でググる。 74

75.

#ccc_a1 まとめ 75

76.

#ccc_a1 Java/DB開発の今どきの手法とツール ● O/Rマッピングライブラリ ○ ソースコード自動生成によるタイプセーフ方式 ○ 外部SQLファイル実行方式 ● 実行したSQLのロギング ● DBマイグレーションの自動化 ● テストデータ投入の自動化 ● テーブル定義書の自動作成 ● トランザクションに気をつけて複数のO/Rマッパーを同時に使用 選択肢と使い方をよく吟味して、レッツ快適開発! 76

77.

#ccc_a1 Thank you ! 77

78.

参考文献 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. Hibernateはどのようにして私のキャリアを破滅寸前にしたか https://www.kaitoy.xyz/2017/02/23/how-hibernate-ruined-my-career/ 上記の原文 https://medium.com/@ggajos/how-hibernate-almost-ruined-16f31ba7d381 我々はいかにして技術選択を間違えたのか? https://blog.cybozu.io/entry/2016/12/28/101500 https://groups.google.com/forum/#!msg/querydsl/fNFXliG8P-k/7dy2aAotVQ0J https://blog.jooq.org/2014/05/29/querydsl-vs-jooq-feature-completeness-vs-now-more-than-ever/ http://dbflute.seasar.org/ja/manual/function/genbafit/implfit/debuglog/index.html https://www.jooq.org/doc/3.11/manual/sql-execution/logging/ http://dbflute.seasar.org/ja/manual/function/ormapper/outsidesql/howto.html https://www.jooq.org/doc/3.11/manual/sql-execution/query-vs-resultquery/ https://www.jooq.org/doc/3.11/manual/sql-building/plain-sql https://flywaydb.org/documentation/migrations#java-based-migrations https://hub.docker.com/r/schemaspy/schemaspy/ #ccc_a1 78