class: center, middle
- うらがみ⛄️
- 大阪の中小SIer
- Doma、JAX-RS(Java EE)
-
JDBCを利用したデータベースアクセスライブラリ --
-
Pluggable Annotation Processing APIを使ってコンパイル時にコード生成したり色んなチェックを行う --
-
コピペでそのまま試せるSQLテンプレート(2 Way SQL)
class: center, middle
-
JARファイルを1つ入れるだけ(
doma-x.y.z.jar
) -- -
他のJARへの依存なし(嬉しい) --
-
Eclipseでは注釈処理の設定をする必要がある --
-
NetBeansは……😢
-
デフォルトではJavaファイルのコンパイル、リソースのコピー、という順 --
-
注釈処理が動く時にSQLファイルのコピーが済んでいなくてコンパイルエラーになる --
-
コンパイルとリソースコピーの順を入れ替える必要がある --
compileJava.dependsOn processResources
-
デフォルトではクラスファイルの出力先とリソースのコピー先が異なる --
-
コンパイル時、リソースのコピー先はクラスパスに含まれず、SQLファイルを検出できない --
-
リソースのコピー先をクラスファイルの出力先と同じにする --
processResources.destinationDir = compileJava.destinationDir
class: center, middle
- 設定クラス
- エンティティ
- エンティティリスナー
- DAO
- SQLファイル
- ドメインクラス
- エンベッダブルクラス
- 設定クラス👈
- エンティティ👈
- エンティティリスナー
- DAO👈
- SQLファイル👈
- ドメインクラス
- エンベッダブルクラス
- 設定クラス
- エンティティ
- エンティティリスナー👈
- DAO
- SQLファイル
- ドメインクラス
- エンベッダブルクラス👈
- 設定クラス
- エンティティ
- エンティティリスナー
- DAO
- SQLファイル
- ドメインクラス👈
- エンベッダブルクラス
DataSource
やDialect
(ページネーションなどで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;
}
int
、String
、byte[]
、java.sql.Date
などLocalDate
、LocalTime
、LocalDateTime
- ドメインクラス
- エンベッダブルクラス
- これらの
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 {
...
}
- エンティティを操作するための
interface
- クエリ発行のエントリポイントにもなる
@Dao
で注釈するconfig
要素にConfig
実装クラスを指定する
@Dao(config = MyConfig.class)
public interface HogeDao {
...
}
Config
を指定しなかったら生成される実装クラスにConfig
を引数にとるコンストラクタが作られる
@Dao
public interface HogeDao {
...
}
public class HogeDaoImpl extends AbstractDao implements HogeDao {
public HogeDaoImpl(Config config) { ... }
...
}
- 引数はわりといろんな型がいける
- 戻り値は基本型、エンティティなど
@Select
Hoge selectById(Long id);
@Select
List<Hoge> selectAll();
- 戻り値の型を
Optional
で包んだり Stream
やCollector
を使用できる
@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
を使用した検索は2種類
@Select(strategy = SelectType.STREAM)
<R> R selectAll(Function<Stream<Hoge>, R> f);
@Select
Stream<Hoge> selectAll();
--
後者の方がシグネチャがすっきり?
使ってみると大差ない
//<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
してくれる。
後者はしてくれない!
try-finally
で囲って使うなど、必ずclose
するよう心がけましょう。
//Stream<Hoge> selectAll();
int total;
try (Stream<Hoge> s = selectAll()) {
total = s.mapToInt(Hoge::getBar).sum();
}
@Insert //@Update @Delete
int insert(Hoge entity);
@Insert(sqlFile = true)
int insert(String foo, LocalDate bar);
@BatchInsert
int[] insert(List<Hoge> entities);
- 2-way SQLが書かれたファイル
- DAOメソッド名に対応したファイルパス
- 例えば
foo.bar.HogeDao.select
メソッドならMETA-INF/foo/bar/HogeDao/select.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;
}
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%"
、100
、200
がバインドされる。
リテラル変数コメント、埋め込み変数コメント
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
public interface SomeDao {
//基本型・ドメインクラス・それらのリスト
//それらのOptionalでなければコンパイルエラー
@Select
List<InvalidClass> selectAll();
}
[DOMA4007] 戻り値のjava.util.Listに対する実型引数の型[InvalidClass]はサポートされていません。
package foo.bar;
@Dao
public interface HogeDao {
//META-INF/foo/bar/HogeDao/selectAll.sql
//が無ければコンパイルエラー
@Select
List<SomeEntity> selectAll();
}
[DOMA4019] ファイル[META-INF/foo/bar/HogeDao/selectAll.sql]がクラスパスから見つかりませんでした。ファイルの絶対パスは"/path/to/META-INF/foo/bar/HogeDao/selectAll.sql"です。
@Select
Hoge selectById(Long id);
SELECT /*%expand*/*
FROM Hoge
-- 下記のように引数を使わないとコンパイルエラー
-- WHERE id = /*id*/1
[DOMA4122] SQLファイル[META-INF/foo/bar/HogeDao/selectById.sql]の妥当検査に失敗しました。メソッドのパラメータ[id]がSQLファイルで参照されていません。
SELECT /*%expand*/*
FROM Hoge
WHERE id = /*id*/1
//引数idが宣言されていないのでコンパイルエラー
@Select
Hoge selectById();
[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環境で使う場合なんかに便利 --
-
DataSource
をLocalTransactionDataSource
でラップする -- -
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(() -> {
//ブロックを抜ければコミット
//ただし、例外で抜ければロールバック
});
-
QueryImplementers
とCommandImplementers
-- -
DAOメソッドは
Query
を組み立てて、それを引数にしてCommand
を組み立てて、Command
を実行する、という流れ -- -
Query
やCommand
のインスタンス化処理をオーバーライドできる。 -- -
SQLの変換や、クエリ発行先の切り替え(検索はスレーブ、更新はマスター、とか)など
--
-
エンベッダブルクラス --
-
Kotlinサポート
--
-
管理すべきもの: 設定クラス、DAO --
-
管理した方が良さそう: エンティティリスナー --
-
管理してはいけないもの: エンティティ、ドメインクラス、エンベッダブルクラス
--
-
Spring Boot(doma-spring-boot-starter) --
-
エンティティをDAO内で定義したい
-
主キー検索クエリは自動で組み立てたい
-
SQLファイルはfoo/bar/HogeDao_select.sql
という名前にしたい
- Author: @backpaper0
- License: The MIT License