とあるDoma2の使い方
+ - 0:00:00
Notes for current slide
Notes for next slide

とあるDoma2の使い方

Doma勉強会 in 東京 2016/07/09

1 / 42

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • 株式会社 Tech to Value
  • Japan Scala Association
2 / 42

今日の内容

うらがみさんのDoma実践が面白かったので、
僕も普段こんな感じでDomaを使ってるよ、
というのを紹介しようと思います。

3 / 42

Immutable Entity

4 / 42

Entity は必ず Immutable に

不変クラスの便利さについては今日は省略。

実はこれだけで語ると40分終わってしまうので ;)

Effective Java にも書かれてるので気になる人は読みましょう。

ともあれ、Entityに限らず、まずクラスは不変で定義して、
理由がある時だけ可変にする、というのを基本にしてます。

5 / 42

Immutable Entity で困るとき

Immutable Entity で困るのは、
IDをDBの自動生成を利用している時

6 / 42

Immutable Entity で困るとき

  1. insertはEntityインスタンスを渡す必要がある
  2. 従ってinsert前にEntityをnewする必要がある
  3. でもIDの値はinsertするまで決定しない
  • 不完全な状態でEntityを作る必要がある
7 / 42

IDクラス

この問題を回避しつつ、型の恩恵を受けるために ID というドメインクラスを定義します。

import org.seasar.doma.Domain;
@Domain(valueType = long.class, factoryMethod = "of")
public final class ID<T> implements Serializable {
private static final long serialVersionUID = 1L;
private final long value;
private ID(final long value) {
this.value = value;
}
public long getValue() {
return value;
}

コンストラクタはprivateに

8 / 42

IDクラス

そして static factory メソッドと 未割り当てを表す notAssigned() を定義します。

public static <R> ID<R> of(final long value) {
if (value < 0) throw new IllegalArgumentException(
"value should be positive. " + value
);
return new ID<>(value);
}
private static final ID<Object> NOT_ASSIGNED = new ID<>(-1);
@SuppressWarnings("unchecked")
public static <R> ID<R> notAssigned() {
return (ID<R>) NOT_ASSIGNED;
}
9 / 42

IDクラス

notAssigned() は notAssigned() 自身とも equals が成り立たないようにする事で、未割り当てのままEntityを活用できないようにします。

@Override
public boolean equals(final Object o) {
if (this == NOT_ASSIGNED) return false;
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ID<?> id = (ID<?>) o;
return value == id.value;
}
@Override
public int hashCode() {
return (int) (value ^ (value >>> 32));
}
}
10 / 42

IDクラス

この IDドメインクラスを使う事で、insert前のID未決定時のEntityインスタンスを作成できるようになります。

final Foo newFoo = new Foo(
ID.notAssigned(),
Name.of("hoge"),
DateTime.now()
);
final Result<Foo> created = FooDao.insert(newFoo);
11 / 42

Collect検索

12 / 42

Collect検索

existを使った存在チェックやcountするような特定のSQLを除いて、 Entityを返す検索メソッドは全て Collect検索にしています。

13 / 42

Collect検索とは

Stream検索 のショートカット。

Stream検索は検索結果を一度に全てjava.util.Listにして受け取るのではなく、java.util.stream.Streamで扱うための検索です。

リソースのクローズがあるので、メソッド引数に Function<Stream<ENTITY>, RESULT> を渡すのが基本になります。

14 / 42

Stream検索の例

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.SelectType;
import java.util.function.Function;
import java.util.stream.Stream;
@Dao(config = AppConfig.class)
public interface FooDao {
@Select(strategy = SelectType.STREAM)
<R> R findAll(final Function<Stream<Foo>, R> func);
}
import static java.util.stream.Collectors.toList;
final List<Foo> foos = fooDao.findAll(s -> s.collect(toList()));
15 / 42

Collect検索

Stream検索は、基本的に大量のレコードを扱うときに便利な検索ですが、collectメソッドを使って値を生成することで、任意の結果を返すことが可能になります。

import static java.util.stream.Collectors.toList;
final List<Foo> foos = fooDao.findAll(s -> s.collect(toList()));

そのため、これをショートカットして、Daoのメソッド引数に直接 java.util.stream.Collector を渡せるようにしたものがCollect検索になります。

16 / 42

Collect検索

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.SelectType;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.Collector;
@Dao(config = AppConfig.class)
public interface FooDao {
@Select(strategy = SelectType.STREAM)
<R> R findAllStream(final Function<Stream<Foo>, R> func);
@Select(strategy = SelectType.COLLECT)
<R> R findAllCollect(final Collector<Foo, ?, R> collector);
}
import static java.util.stream.Collectors.toList;
final List<Foo> foos1 = fooDao.findAllStream(s -> s.collect(toList()));
final List<Foo> foos2 = fooDao.findAllCollect(toList()); // 同じ意味
17 / 42

Collect検索

ちょっとしたショートカットと言えばそれまでですが、
Stream検索でラムダ構文を使うより型推論がしやすくなるケースが多い印象です。

18 / 42

Collect検索

で、(Entityを返す)全ての検索をCollect検索にしています

19 / 42

1件検索

例えば、よくある主キーで1件検索する findById みたいなメソッド。あれもCollect検索を使います。

import java.util.Optional;
import java.util.stream.Collector;
import static java.util.Collections.singleton;
import static jp.t2v.lab.domasample.Collectors2.toOptional;
@Dao(config = AppConfig.class)
public interface FooDao {
@Select(strategy = SelectType.COLLECT)
<R> R findByIds(final Iterable<ID<Foo>> ids,
final Collector<Foo, ?, R> collector);
default Optional<Foo> findById(ID<Foo> id) {
return findByIds(singleton(id), toOptional());
}
}
20 / 42

1件検索

Stream#findAny に相当するような Collector が標準で存在していれば良かったのですが、存在しないので Optional を返す Collector、toOptional を定義します。

import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collector;
public class Collectors2 {
private Collectors2() {}
public static <T> Collector<T, ?, Optional<T>> toOptional() {
return Collector.<T, AtomicReference<T>, Optional<T>>of(
AtomicReference::new,
(acc, t) -> acc.compareAndSet(null, t),
(a, b) -> {a.compareAndSet(null, b.get()); return a;},
acc -> Optional.ofNullable(acc.get()),
Collector.Characteristics.CONCURRENT
);
}
}
21 / 42

1件検索

なぜわざわざそんな事をしているかと言うと、以下の理由があります。

  • one-to-manyなどの構造を表現する際にCollect検索を多用する(後述)
  • 1件検索と複数件検索で同じSQLファイルを使用したい
22 / 42

同じSQLファイルを使用したい

https://twitter.com/nakamura_to/status/548833863363883010

えらい人は言いました。「再利用性よりもSQLの完全性」と。

23 / 42

同じSQLファイルを使用したい

where句なんかは洩れがあると大変なので、なるべく共通化したいです。(複数検索ではちゃんと削除フラグを条件で見てたのに1件検索では忘れてたとか目も当てられない)

けれども外だしSQLファイルではSQLの完全性が大事。where句のincludesとかしだすとすぐにSQLがメンテできなくなっていきます。

であれば完全なSQLファイルをそのまま再利用すればいい!

24 / 42

同じSQLファイルを使用したい

という訳で、1件検索にもCollect検索を利用することで、SQLファイルを再利用する事ができるようになります。

25 / 42

デメリット

検索結果保証機能や2件以上存在したときのNonUniqueResultExceptionなどの機能が使えなくなります。

ただこれも、Collectorの実装を工夫すれば対応可能です。

26 / 42

もう一つの理由

27 / 42

もう一つの理由

1件検索でもCollect検索を使う理由を二つ挙げていました。(再掲)

  • one-to-manyなどの構造を表現する際にCollect検索を多用する
  • 1件検索と複数件検索で同じSQLファイルを使用したい

構造を表現する際にCollect検索があるととても楽ができます。

28 / 42

Domaの検索

Domaの検索は、基本的にResultSetの1行を、一つのEntityクラスにマッピングします。

従って、n:m の関係だったり 1:0-1 の関係だったりといった複雑なオブジェクト構造を直接マッピングする事ができません。

そこで、Domaでそういった複雑なオブジェクト構造を作る場合には、Entity毎に検索を行い検索結果を手で組み立てます。

29 / 42

Domaの検索

Entity毎に検索を行えば n:m の関係を下手に join で扱って limmit/offset する時に困るというのもなくなります。

ただ、単純にrootのEntityを取得し、その一つずつ関連するEntityの検索を行ってしまうと簡単にN+1問題 が発生します。

その時にCollect検索の出番です。

30 / 42

オブジェクト構造の例

例えば以下の様なゲームのEntityの関係を考えます。

Guild : 1 ------ N : Character
Guild : 1 ------ 0-1 : GuildHouse

GuildCharacterGuildHouseがそれぞれEntityです。

31 / 42

オブジェクト構造の例

これを以下のようなクラスにマッピングします。

import java.util.List;
import java.util.Optional;
import static java.util.Collections.unmodifiableList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
public class GuildView {
private final Guild meta;
private final List<Character> members;
private final Optional<GuildHouse> house;
public GuildView(final Guild meta,
final List<? extends Character> members,
final Optional<? extends GuildHouse> house) {
this.meta = meta;
this.members = unmodifiableList(members.stream().collect(toList()));
this.house = house.map(identity());
}
// getterその他省略
32 / 42

オブジェクト構造の例

GuildDao

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.SelectType;
import org.seasar.doma.jdbc.SelectOptions;
import java.util.stream.Collector;
@Dao(config = AppConfig.class)
public interface GuildDao {
@Select(strategy = SelectType.COLLECT)
<R> R findAll(final SelectOptions opt,
final Collector<Guild, ?, R> collector);
33 / 42

オブジェクト構造の例

CharacterDao

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.SelectType;
import java.util.stream.Collector;
@Dao(config = AppConfig.class)
public interface CharacterDao {
@Select(strategy = SelectType.COLLECT)
<R> R findByGuildIds(final Iterable<ID<Guild>> guildIds,
final Collector<Character, ?, R> collector);
34 / 42

オブジェクト構造の例

CharacterDao/findByGuildIds.sql

SELECT
*
FROM
`character`
WHERE
guild_id IN /*guildIds*/(1, 2)
AND deleted_time IS NULL
35 / 42

オブジェクト構造の例

GuildHouseDao

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.SelectType;
import java.util.stream.Collector;
@Dao(config = AppConfig.class)
public interface GuildHouseDao {
@Select(strategy = SelectType.COLLECT)
<R> R findByGuildIds(final Iterable<ID<Guild>> guildIds,
final Collector<GuildHouse, ?, R> collector);
36 / 42
import j.u.{List, Map, Optional};
import static j.u.Collections.emptyList;
import static j.u.function.Function.identity;
import static j.u.stream.Collectors.{toList, toMap, toSet, groupingBy};
final SelectOptions opt = SelectOptions.get().limit(100).offset(0);
final List<Guild> guilds = guildDao.findAll(opt, toList());
final Set<ID<Guild>> ids =
guilds.stream().map(Guild::getId).collect(toSet());
final Map<ID<Guild>, List<Character>> chars = // 1:N は Map<ID, List> で
characterDao.findByGuildIds(ids, groupingBy(Character::getGuildId));
final Map<ID<Guild>, GuildHouse> houses = // 1:0-1 は Map<ID, Entity> で
guildHouseDao.findByGuildIds(ids,
toMap(GuildHouse::getGuildId, identity()));
final List<GuildView> views =
guilds.stream().map(buildGuild(chars, houses)).collect(toList());
private Function<Guild, GuildView> buildGuild(
final Map<ID<Guild>, List<Character>> characters,
final Map<ID<Guild>, GuildHouse> houses) {
return guild -> new GuildView(
guild,
characters.getOrDefault(guild.getId(), emptyList()),
Optional.ofNullable(houses.get(guild.getId())) // getOpt()って書きたい
);
}
37 / 42

Doma2 の嬉しい点

final Set<ID<Guild>> ids =
guilds.stream().map(Guild::getId).collect(toSet());
final Map<ID<Guild>, List<Character>> chars =
characterDao.findByGuildIds(ids, groupingBy(Character::getGuildId));

idsが空だった時、Doma1だとcharacterDao.findByGuildIds で IN句が空になりSQL構文エラー!

Doma2では、空集合が渡されると IN(null) に変換されるため安全に!

38 / 42

その他の便利な変換の例

指定したIDのキャラクターの所属ギルドのID一覧を取得

import static j.u.stream.Collectors.{mapping, toSet};
final Set<ID<Guild>> ids =
characterDao.findByIds(asList(ID.of(1), ID.of(2)),
mapping(Character::getGuildId, toSet()));

Collectors.mapping を使えば、 .stream().map(foo).collect(bar).stream().collect(mapping(foo, bar)) に書き換えられる。

39 / 42

Collect検索まとめ

  • Collect検索使うと、Listで取りたい時でも、Optionalで取りたい時でも、Mapで取りたい時でも他の操作をしたいときでも自由自在で便利!
40 / 42

全体まとめ

  • DomaはImmutableなEntityサポートしてて便利です
  • Domain Class は色々工夫できます
  • (Entityを受け取る)検索は全部Collect検索にしましょう
41 / 42

質問とか

42 / 42

自己紹介

  • 中村 学(Nakamura Manabu)
  • @gakuzzzz
  • 株式会社 Tech to Value
  • Japan Scala Association
2 / 42
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow