Skip to content

Latest commit

 

History

History
913 lines (620 loc) · 18.5 KB

doma-intro.md

File metadata and controls

913 lines (620 loc) · 18.5 KB

class: center, middle

Domaの紹介


の前に、自己紹介

  • うらがみ⛄️
  • 大阪の中小SIer
  • Doma、JAX-RS(Java EE)

Domaとは

  • JDBCを利用したデータベースアクセスライブラリ --

  • Pluggable Annotation Processing APIを使ってコンパイル時にコード生成したり色んなチェックを行う --

  • コピペでそのまま試せるSQLテンプレート(2 Way SQL)


class: center, middle

まずはセットアップ


セットアップ

  • JARファイルを1つ入れるだけ(doma-x.y.z.jar) --

  • 他のJARへの依存なし(嬉しい) --

  • Eclipseでは注釈処理の設定をする必要がある --

  • Doma Tools(Eclipse) --

  • DomaSupport(IntelliJ IDEA) --

  • NetBeansは……😢


Gradleでビルド

ちょっと工夫が必要

  • デフォルトではJavaファイルのコンパイル、リソースのコピー、という順 --

  • 注釈処理が動く時にSQLファイルのコピーが済んでいなくてコンパイルエラーになる --

  • コンパイルとリソースコピーの順を入れ替える必要がある --

compileJava.dependsOn processResources

Gradleでビルド

  • デフォルトではクラスファイルの出力先とリソースのコピー先が異なる --

  • コンパイル時、リソースのコピー先はクラスパスに含まれず、SQLファイルを検出できない --

  • リソースのコピー先をクラスファイルの出力先と同じにする --

processResources.destinationDir = compileJava.destinationDir

class: center, middle

各クラスと役割


作るクラスとか

  • 設定クラス
  • エンティティ
  • エンティティリスナー
  • DAO
  • SQLファイル
  • ドメインクラス
  • エンベッダブルクラス

絶対に必要なもの

  • 設定クラス👈
  • エンティティ👈
  • エンティティリスナー
  • DAO👈
  • SQLファイル👈
  • ドメインクラス
  • エンベッダブルクラス

無くても良いけどあると便利なもの

  • 設定クラス
  • エンティティ
  • エンティティリスナー👈
  • DAO
  • SQLファイル
  • ドメインクラス
  • エンベッダブルクラス👈

無くても良いけど無いと耐えられないもの

  • 設定クラス
  • エンティティ
  • エンティティリスナー
  • DAO
  • SQLファイル
  • ドメインクラス👈
  • エンベッダブルクラス

設定クラス

  • DataSourceDialect(ページネーションなどでRDBMS間の方言を吸収するためのinterface)を設定するクラス
  • 通常、アプリケーション内で使用するDBにつき1つ必要

設定クラスの作り方

  • Configを実装する
public class MyConfig implements Config {

    @Override
    public DataSource getDataSource() { ... }

    @Override
    public Dialect getDialect() { ... }

    //必要に応じて他のメソッドもoverrideする
}

シングルトンな設定クラス

  • @SingletonConfigで注釈すればファクトリーメソッドでインスタンスを取得することもできる
@SingletonConfig
public class MyConfig implements Config {

    public static MyConfig singleton() { ... }

    //コンストラクタはprivateにする
    private MyConfig() {}

    ...
}

エンティティ

  • テーブルや検索結果にマッピングするクラス
  • クラスがテーブル(検索結果)、フィールドがカラムに対応する

エンティティの作り方

  • @Entityで注釈する
  • アクセサは無くてもOK
@Entity
public class Hoge {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;
    public String foo;
    public LocalDate bar;
}

エンティティのフィールドに使える型

  • intStringbyte[]java.sql.Dateなど
  • LocalDateLocalTimeLocalDateTime
  • ドメインクラス
  • エンベッダブルクラス
  • これらのOptional<T>

イミュータブルなエンティティ

  • immutable要素をtrueにする
@Entity(immutable = true)
public class Hoge {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public final Long id;
    public final String foo;
    public final LocalDate bar;
    public Hoge(Long id, String foo, LocalDate bar) {
        ...
    }
}

エンティティリスナー

  • 挿入・更新・削除の前後に処理を挟む

エンティティリスナーの作り方

public class HogeListener implements EntityListener<Hoge> {

    @Override
    public void preInsert(Hoge entity,
                          PreInsertContext<Hoge> context) {
        entity.createdAt = LocalDateTime.now();
    }

    //preUpdate, preDelete
    //postInsert, postUpdate, postDelete
}

エンティティリスナーの利用設定

  • エンティティの@Entityで利用の設定を行う
@Entity(listener = HogeListener.class)
public class Hoge {
    ...
}

DAO

  • エンティティを操作するためのinterface
  • クエリ発行のエントリポイントにもなる

DAOの作り方

  • @Daoで注釈する
  • config要素にConfig実装クラスを指定する
@Dao(config = MyConfig.class)
public interface HogeDao {
    ...
}

Configを指定しないDAO

  • Configを指定しなかったら生成される実装クラスにConfigを引数にとるコンストラクタが作られる
@Dao
public interface HogeDao {
    ...
}
public class HogeDaoImpl extends AbstractDao implements HogeDao {
    public HogeDaoImpl(Config config) { ... }
    ...
}

検索系DAOメソッド

  • 引数はわりといろんな型がいける
  • 戻り値は基本型、エンティティなど
@Select
Hoge selectById(Long id);
@Select
List<Hoge> selectAll();

Java 8に対応したDAOメソッド

  • 戻り値の型をOptionalで包んだり
  • StreamCollectorを使用できる
@Select
Optional<Hoge> selectById(Long id);
@Select(strategy = SelectType.STREAM)
<R> R selectAll(Function<Stream<Hoge>, R> f);
@Select(strategy = SelectType.COLLECT)
<R> R selectAll(Collector<Hoge, ?, R> collector);

Stream検索

Streamを使用した検索は2種類

@Select(strategy = SelectType.STREAM)
<R> R selectAll(Function<Stream<Hoge>, R> f);
@Select
Stream<Hoge> selectAll();

--

後者の方がシグネチャがすっきり?


Stream検索

使ってみると大差ない

//<R> R selectAll(Function<Stream<Hoge>, R> f);
int total = selectAll(s -> s.mapToInt(Hoge::getBar).sum());
//Stream<Hoge> selectAll();
int total = selectAll().mapToInt(Hoge::getBar).sum();

--

前者はResultSetを自動でcloseしてくれる。

後者はしてくれない!


Stream検索

try-finallyで囲って使うなど、必ずcloseするよう心がけましょう。

//Stream<Hoge> selectAll();
int total;
try (Stream<Hoge> s = selectAll()) {
    total = s.mapToInt(Hoge::getBar).sum();
}

更新系DAOメソッド

@Insert //@Update @Delete
int insert(Hoge entity);
@Insert(sqlFile = true)
int insert(String foo, LocalDate bar);
@BatchInsert
int[] insert(List<Hoge> entities);

SQLファイル

  • 2-way SQLが書かれたファイル
  • DAOメソッド名に対応したファイルパス
  • 例えばfoo.bar.HogeDao.selectメソッドならMETA-INF/foo/bar/HogeDao/select.sql

2-way SQLの書き方

  SELECT /*%expand*/*
    FROM Hoge
   WHERE foo = /* foo */'a'
     AND bar LIKE /* @prefix(bar) */'b%'
     AND baz IN /* cond.bazList */(1, 2, 3)
@Select
List<Hoge> select(String foo, String bar, Cond cond);

public static class Cond {
    public List<Integer> bazList;
}

発行されるプリペアードSQL

Cond cond = new Cond();
cond.bazList = Arrays.asList(100, 200);
dao.select("a", "b", cond);
  SELECT foo, bar, baz
    FROM Hoge
   WHERE foo = ?
     AND bar LIKE ?
     AND baz IN (?, ?)

"a""b%"100200がバインドされる。


その他の埋め込み

リテラル変数コメント、埋め込み変数コメント

WHERE foo = /*^ foo */'x'
/*# orderBy */
dao.select("a", "ORDER BY foo ASC");

--

プレースホルダではなく、SQLに直接埋め込まれる。

WHERE foo = 'a'
ORDER BY foo ASC

条件分岐、ループ

WHERE
/*if foo != null*/
    foo = /* foo */'x'
/*end*/

and (
/*%for bar : bars */
    bar = /* bar */'y'
    /*%if bar_has_next */
        /*# "or" */
    /*%end */
/*%end*/
)

他にも

  • バインドされる変数のメソッドを呼べたり --

  • staticメソッドを呼べたり --

  • ifコメントの条件を論理演算で幾つも書けたり --

  • もちろん、コンパイル時に型チェックしてくれる --

  • でも、やりすぎると大変なのでほどほどに


ドメインクラス

  • 基本型を具体的な型に落とし込むためのクラス
  • 例:「郵便番号」をStringではなくZipCode

ドメインクラスの作り方

  • @Domainで注釈してvalueType要素に基本型を指定する
@Domain(valueType = String.class)
public class Fuga {
    private final String value;
    public Fuga(String value) {
        this.value = Objects.requireNonNull(value);
    }
    public String getValue() {
        return value;
    }
}

外部ドメイン

  • 編集できない既存クラスもドメインクラス化
@ExternalDomain
public class PathConverter implements
                DomainConverter<Path, String> {
    @Override
    public String fromDomainToValue(Path domain) {
        return Optional.ofNullable(domain)
            .map(Path::toString).orElse(null);
    }
    @Override
    public Path fromValueToDomain(String value) {
        return Optional.ofNullable(value)
            .map(Paths::get).orElse(null);
    }
}

外部ドメインの利用方法

コンバータをまとめたクラスを作って、

@DomainConverters({ PathConverter.class })
public class DomainConvertersProvider {
}

--

コンパイル時の注釈処理オプションで指定する。

javac -Adoma.domain.converters=foo.bar.DomainConvertersProvider ...

エンベッダブルクラス

  • 複数のカラムをまとめたクラス
  • 例:「都道府県」「市区町村」「番地」をまとめて「住所」のエンベッダブルクラスを作る

エンベッダブルクラスの作り方

  • @Embeddableで注釈する
@Embeddable
public class Password {
    private final PasswordHash hash;
    private final Salt salt;
    private final HashAlgorithm hashAlgorithm;

    public Password(PasswordHash hash, Salt salt, HashAlgorithm hashAlgorithm) {
        ...
    }

class: center, middle

コンパイル時チェック


注釈処理でコンパイル時に色々チェック

  • Domaに慣れないうちはコンパイルエラーを解消していくことでDomaを学べる(はず)
  • 手厚いチェックと分かりやすいエラーメッセージ

エンティティのフィールドが扱えない型だとコンパイルエラー

@Entity
public class SomeEntity {
    //基本型・ドメインクラス・エンベッダブルクラス
    //でなければコンパイルエラー
    public InvalidClass field;
}

エンティティのフィールドが扱えない型だとコンパイルエラー

[DOMA4096] クラス[InvalidClass]は、永続対象の型としてサポートされていません。 at SomeEntity.field。@ExternalDomainでマッピングすることを意図している場合、登録や設定が不足している可能性があります。@DomainConvertersを注釈したクラスと注釈処理のオプション(doma.domain.converters)を見直してください。


DAOメソッドの戻り値が扱えない型だとコンパイルエラー

@Dao
public interface SomeDao {

    //基本型・ドメインクラス・それらのリスト
    //それらのOptionalでなければコンパイルエラー
    @Select
    List<InvalidClass> selectAll();
}

DAOメソッドの戻り値が扱えない型だとコンパイルエラー

[DOMA4007] 戻り値のjava.util.Listに対する実型引数の型[InvalidClass]はサポートされていません。


DAOメソッドに対応したSQLファイルがクラスパス上に無いとコンパイルエラー

package foo.bar;

@Dao
public interface HogeDao {

    //META-INF/foo/bar/HogeDao/selectAll.sql
    //が無ければコンパイルエラー
    @Select
    List<SomeEntity> selectAll();
}

DAOメソッドに対応したSQLファイルがクラスパス上に無いとコンパイルエラー

[DOMA4019] ファイル[META-INF/foo/bar/HogeDao/selectAll.sql]がクラスパスから見つかりませんでした。ファイルの絶対パスは"/path/to/META-INF/foo/bar/HogeDao/selectAll.sql"です。


DAOメソッドの引数をクエリで使用していないとコンパイルエラー

@Select
Hoge selectById(Long id);
  SELECT /*%expand*/*
    FROM Hoge
-- 下記のように引数を使わないとコンパイルエラー
-- WHERE id = /*id*/1

DAOメソッドの引数をクエリで使用していないとコンパイルエラー

[DOMA4122] SQLファイル[META-INF/foo/bar/HogeDao/selectById.sql]の妥当検査に失敗しました。メソッドのパラメータ[id]がSQLファイルで参照されていません。


クエリで使用しているバインド変数がDAOメソッドで宣言されていないとコンパイルエラー

SELECT /*%expand*/*
  FROM Hoge
 WHERE id = /*id*/1
//引数idが宣言されていないのでコンパイルエラー
@Select
Hoge selectById();

クエリで使用しているバインド変数がDAOメソッドで宣言されていないとコンパイルエラー

[DOMA4092] SQLファイル[META-INF/foo/bar/HogeDao/selectById.sql]の妥当検査に失敗しました([3]行目[18]番目の文字付近)。詳細は次のものです。[DOMA4067] SQL内の変数[id]に対応するパラメータがメソッドに存在しません([2]番目の文字付近)。 SQL[SELECT /%expand/* FROM Hoge WHERE id = /id/1 ]。


ドメインクラスに必要なファクトリーメソッドや、アクセサが無いとコンパイルエラー

@Domain(valueType = String.class)
public class Hoge {
    private String value;

    //Stringを受け取るコンストラクタが
    //無いとコンパイルエラー

    //getValueが無いとコンパイルエラー
}

ドメインクラスに必要なファクトリーメソッドや、アクセサが無いとコンパイルエラー

[DOMA4103] 型[java.lang.String]をパラメータにもつ非privateなコンストラクタが見つかりません。コンストラクタを定義するか、ファクトリメソッドを利用したい場合は@DomainのfactoryMethod属性にメソッド名を指定してください。


ドメインクラスに必要なファクトリーメソッドや、アクセサが無いとコンパイルエラー

[DOMA4104] アクセッサメソッド[getValue]が見つかりません。アクセッサメソッドは、型[java.lang.String]を戻り値とする非privateで引数なしのインスタンスメソッドでなければいけません。 at Hoge


class: center, middle

その他の話題


ローカルトランザクション

  • スレッドに紐付けてトランザクションを扱うAPI --

  • Java SE環境で使う場合なんかに便利 --

  • DataSourceLocalTransactionDataSourceでラップする --

  • TransactionManager経由で使うのが楽


ローカルトランザクションを使う準備

private LocalTransactionDataSource dataSource;

public MyConfig() {
    DataSource original = ...
    dataSource = new LocalTransactionDataSource(original);
}

@Override
public DataSource getDataSource() { return dataSource; }

@Override
public TransactionManager getTransactionManager() {
    return new LocalTransactionManager(
        dataSource.getLocalTransaction(getJdbcLogger()));
}

ローカルトランザクションを使う

ローンパターンでトランザクション境界を作っている。

Config config = Config.get(dao);
TransactionManager tm = config.getTransactionManager();
tm.required(() -> {
    //ブロックを抜ければコミット
    //ただし、例外で抜ければロールバック
});

拡張ポイント

  • QueryImplementersCommandImplementers --

  • DAOメソッドはQueryを組み立てて、それを引数にしてCommandを組み立てて、Commandを実行する、という流れ --

  • QueryCommandのインスタンス化処理をオーバーライドできる。 --

  • SQLの変換や、クエリ発行先の切り替え(検索はスレーブ、更新はマスター、とか)など


最近のDoma

--

  • エンベッダブルクラス --

  • Kotlinサポート


DIコンテナとDoma

--

  • 管理すべきもの: 設定クラス、DAO --

  • 管理した方が良さそう: エンティティリスナー --

  • 管理してはいけないもの: エンティティ、ドメインクラス、エンベッダブルクラス


Doma統合

--


最後にしれっと個人的な要望


この資料について