TL;DR
- jOOQで自動生成したO/Rマッピング用のJavaソースコードは、git管理してはいけない。 .gitignoreの対象にしておきましょう。
- さもないとチーム開発のときにコンフリクトが起きやすくなり、DBの変更(テーブルやカラムの追加、変更、etc)に強いというjOOQのメリットを生かせなくなります。
- ローカル開発環境のセットアップ、つまりDocker, gradle-jooq-plugin, flyway-gradle-pluginをきちんと使えば上記はラクに実現可能
- ついでにDDD(ドメイン駆動設計)的な意味では、jOOQで自動生成したエンティティクラスをそのままドメインクラスとして使うのは悪手。面倒でも自作したドメインクラスに値を詰め替えましょう。
- サンプルコードはこちら
- なお、jOOQの読み方は「ジューク」です。
Detail
2018年12月のJJUG-CCC(日本Javaユーザーズグループクロスコミュニティカンファレンス)でツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところというタイトルで登壇させていただきました。
数あるO/RマッパーやDBマイグレーションツールの中でも、jOOQ推し、Flyway推しという内容でした。一方で巷のブログや人づてに聞く話ではありますが、それらの使い方を若干間違えているケースが多々あるようです。 特にまずいのが、jOOQが自動生成したJavaソースコードをgitのコミット対象としてバージョン管理しているケースがそこそこ観測されることです。それはやめたほうがいいです。 複数の開発者がうっかり同時期に同じテーブルに対してカラム追加等をすると、jOOQで自動生成した大量のJavaコード群は必ずコンフリクトを起こしてしまうからです。
jOOQは、指定したDBスキーマの構造を正確無比に読み取って、O/RマッピングをラクにするJavaコードを自動生成するのがキモです。 ということは、正確無比にバージョン管理しておくべきなのはDBスキーマ定義つまりCREATE TABLE, ALTER TABLEなどいわゆるDDL文のほうです。そこを実現するのがFlywayです。
jOOQが自動生成するのはJavaコードですので、つい src/main/java の配下を出力先として指定したくなりますが、そこが落とし穴です。gradleを使っている場合は build/の配下をjOOQの自動生成コードの出力先として指定したうえで、ソースコードの在り処も指定することで、コンパイルは通ります。IntelliJ IDEA上でも問題はありません。ただしEclipseという古代兵器のことは存じません。
サンプルコードを動かしてみる
git, JDK8以上, dockerが入ったPCを用意して、下記のコマンドを実行するだけです。
git clone git@github.com:nabedge/jooq-flyway-spboot-sample.git
cd jooq-flyway-spboot-sample
sh setup.sh
sh ./gradlew run -p pj-web
ブラウザで http://localhost:8080 を開く
サンプルコードをIntelliJ IDEAにプロジェクトとしてインポートしてみる
- 上の手順で sh setup.sh を実行するところまでは済ませておく
- File -> New -> Project from existing sources
- jooq-flyway-spboot-sample ディレクトリ直下の build.gradle を指定してOpen
- use auto importにチェックを入れてOKボタンを押す
- pj-webプロジェクトの SampleApplication.java をクリックして起動
- ブラウザで http://localhost:8080 を開く
Dockerの使い方のポイント
- PostgreSQLの公式イメージの、docker-entrypoint-initdb.d ディレクトリ配下にある *.sql ファイルはよしなに実行してくれる機能を使う
- ただしそこではアプリケーション用のデータベースそのものとそのデータベースユーザーとを作るにとどめる
- つまり、アプリケーションは、カラのDBと、そこにアクセスを許可されたアプリケーション用のDBユーザーだけが既に存在する前提で開発する。
Flywayの使い方のポイント
- セオリーどおり、 src/main/resources/db/migration の配下に、一定の命名方法で *.sqlファイルを用意します。中身はCREATE TABLE文等です。
- ローカル開発環境とはいえ、ある程度のテストデータがテーブルに入ってないと、実際の開発作業ではとても不便です。そこで、src/test/resources/testdataの配下に、テストデータ投入用のINSERT文を並べたファイルを用意しておきましょう
- テストデータが100件くらいほしいのに、100行のINSERT文を書くのは面倒です。ならば、src/test/java/testdataの配下に、適当にループしながらテストデータをINSERTするJavaコードを書いてしまいましょう。これが FlywayのJava Based Migration という機能です。
- Docker上のPostgreSQLには、./gradlew flywayMigrate -p pj-db コマンドでテーブルが自動的に作成されます。Flywayは「対象のDBにはどのSQL(DDL)文が実行済みなのか?」を正確に把握してSQLを実行しますので、このコマンドを本番環境でも同様に安心して実行できます。
- 既に本番稼働中のデータベース/アプリケーションに途中からFlywayを導入する場合は途中からFlywayでググりましょう。いろいろ出てきます。
- テストデータは ./gradlew flywayMigrate -Dflyway.locations=classpath:testdata で投入されます。-Dflyway.locationsオプションを明示的に与えない限りはこれらの配下のスクリプトが動くことはないため、本番環境等にテストデータが入ってしまう事故は十分に防げます。
jOOQの使い方のポイント
- しつこいようですがjOOQの読み方はジュークです。
- jOOQとFlywayを使用するプロジェクトでは、それらがターゲットとするDBアクセス用のライブラリをサブプロジェクトとして分離するのが基本です。通常のアプリケーションコードとごちゃまぜにすると、ビルド定義ファイル(build.gradle)が無用に複雑になって長気に渡る戦乱の発端となるからです。 必ずマルチプロジェクト構成。これ基本です。
- サンプルプロジェクトでは、 pj-db と pj-db-custom-strategy がDBアクセス関連用のサブプロジェクトです。メインアプリケーションであるpj-webがpj-dbに依存する形で開発すればよいです。
- jOOQによるSQL文の発行のサンプルはBookRepository.javaです。
- jOOQにJavaコードを自動生成させるには gradle-jooq-pluginを使用します。
- pj-db/build.gradleのこのあたりに、jooqRuntimeという見慣れない定義があります。これらが、jOOQがDBにアクセスし、そこに張ってあるテーブル定義を全て解読してO/Rマッピング用のJavaコードを生成してくれます。
jOOQとbuild.gradle
このあたりから下だけでももう少し解説しましょう。
jooq {
version = "${jooqVersion}"
edition = 'OSS' // if you use oracle, you should pay :-)
// the name "sample" -> task name "generateSampleJooqSchemaSource" . see below.
sample (sourceSets.main) {
jdbc {
driver = "${jdbcDriver}"
url = "${dbUrl}"
user = "${dbUser}"
password = "${dbPassword}"
}
generator {
target {
packageName = "${jooqDestPackage}"
directory = "${jooqDestDir}"
}
strategy {
name = 'com.example.db.jooq.generator.SamplePrefixGeneratorStrategy'
}
database() {
name = 'org.jooq.meta.postgres.PostgresDatabase'
inputSchema = "public"
}
generate() {
daos = true
immutablePojos = true
pojosEqualsAndHashCode = true
}
}
}
}
compileJava {
dependsOn generateSampleJooqSchemaSource
sourceSets.main.java.srcDirs(jooqDestDir)
}
- "sample"という値はこのサンプルコード上でのDB定義の抽象名だと思ってください。 これはそのまま generateSampleJooqSchemaSource というタスク名になります。
- compileJavaタスクの前提条件(dependsOn)としてgenerateSampleJooqSchemaSourceがあるため、gradleは必ずコードの自動生成をしたうえでそれをコンパイルしようとします。また、sourceSets.main.java.srcDirs(jooqDestDir) によって、ソースコードのありかは src/main/java ではなく、冒頭で定義されている build/jooq-gen にソースコードが自動生成されています。
- 当然 、"build"ディレクトリ自体がは.gitignoreに指定されています
jOOQが自動生成するコードのクラス名にプレフィクス/サフィックスをつける
ツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところ の50ページ付近で話したとおり、デフォルトではjOOQはテーブル名とまったく同じ名前のJavaクラスを作ってしまいます。 カスタムを強く推奨します。 クラス名の衝突はコーディング作業中のけっこうなストレスにつながります。
そこで、上のbuild.gradleのこの部分
strategy {
name = 'com.example.db.jooq.generator.SamplePrefixGeneratorStrategy'
}
が重要になってきます。このクラスは pj-db-custom-strategyというサブプロジェクトのたった一つのクラスとして実装されています。
import org.jooq.codegen.DefaultGeneratorStrategy;
import org.jooq.meta.Definition;
public class SamplePrefixGeneratorStrategy 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";
case DEFAULT:
return 'J' + name;
}
return name;
}
見ての通り、jOOQが自動生成するJavaクラスのプレフィクスとして全て”J”がつくようになっています。これによって、実際のDBアクセスのコードでは
final JBook jBook = JBook.BOOK;
final List<BookVo> selected = dslContext
.select(
jBook.ISBN,
jBook.TITLE,
jBook.PUBLISH_DATE
)
.from(jBook)
.orderBy(jBook.PUBLISH_DATE)
.fetchInto(BookVo.class);
// .fetchInto(Book.class); // or you can use original class directly !
return selected
.stream()
.map(bookVo -> {
Book book = new Book();
book.setIsbn(bookVo.getIsbn());
book.setTitle(bookVo.getTitle());
book.setPublishDate(bookVo.getPublishDate().toLocalDate());
return book;
})
.collect(Collectors.toList());
このように、
- jOOQが自動生成したO/Rマッパ用のコード(JBook.java)
- 同じく自動生成したエンティティクラス(BookVo.java)
- 自作するべきDDD的なクラス(Book.java)
これらの名前衝突を避けることができるようになります。
おわりに
- 昨年の資料 ツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところ
- 今月作ったサンプルコード
- このブログ
それぞれ読みながらサンプルを動かしてIDEにインポートしてみれば、勘所がおわかりいただけるかと思います。
Happy Hacking !
なお、この記事は独自ブログからの執筆者本人による転載です。 -> https://nabedge.mixer2.org/2019/03/jooq-flyway-sample.html