Tags: java

TL;DR

画像

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にプロジェクトとしてインポートしてみる

  1. 上の手順で sh setup.sh を実行するところまでは済ませておく
  2. File -> New -> Project from existing sources
  3. jooq-flyway-spboot-sample ディレクトリ直下の build.gradle を指定してOpen
  4. use auto importにチェックを入れてOKボタンを押す
  5. pj-webプロジェクトの SampleApplication.java をクリックして起動
  6. ブラウザで http://localhost:8080 を開く

Dockerの使い方のポイント

Flywayの使い方のポイント

jOOQの使い方のポイント

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)
}

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());

このように、

これらの名前衝突を避けることができるようになります。

おわりに

それぞれ読みながらサンプルを動かしてIDEにインポートしてみれば、勘所がおわかりいただけるかと思います。
Happy Hacking !