Doma勉強会 in 東京 2016/07/09
不変クラスの便利さについては今日は省略。
実はこれだけで語ると40分終わってしまうので ;)
Effective Java にも書かれてるので気になる人は読みましょう。
ともあれ、Entityに限らず、まずクラスは不変で定義して、
理由がある時だけ可変にする、というのを基本にしてます。
Immutable Entity で困るのは、
IDをDBの自動生成を利用している時
この問題を回避しつつ、型の恩恵を受けるために 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に
そして 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; }
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)); }}
この IDドメインクラスを使う事で、insert前のID未決定時のEntityインスタンスを作成できるようになります。
final Foo newFoo = new Foo( ID.notAssigned(), Name.of("hoge"), DateTime.now());final Result<Foo> created = FooDao.insert(newFoo);
Stream検索 のショートカット。
Stream検索は検索結果を一度に全てjava.util.List
にして受け取るのではなく、java.util.stream.Stream
で扱うための検索です。
リソースのクローズがあるので、メソッド引数に Function<Stream<ENTITY>, RESULT>
を渡すのが基本になります。
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()));
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検索になります。
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()); // 同じ意味
ちょっとしたショートカットと言えばそれまでですが、
Stream検索でラムダ構文を使うより型推論がしやすくなるケースが多い印象です。
で、(Entityを返す)全ての検索をCollect検索にしています
例えば、よくある主キーで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()); }}
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 ); }}
なぜわざわざそんな事をしているかと言うと、以下の理由があります。
えらい人は言いました。「再利用性よりもSQLの完全性」と。
where句なんかは洩れがあると大変なので、なるべく共通化したいです。(複数検索ではちゃんと削除フラグを条件で見てたのに1件検索では忘れてたとか目も当てられない)
けれども外だしSQLファイルではSQLの完全性が大事。where句のincludesとかしだすとすぐにSQLがメンテできなくなっていきます。
であれば完全なSQLファイルをそのまま再利用すればいい!
という訳で、1件検索にもCollect検索を利用することで、SQLファイルを再利用する事ができるようになります。
検索結果保証機能や2件以上存在したときのNonUniqueResultExceptionなどの機能が使えなくなります。
ただこれも、Collectorの実装を工夫すれば対応可能です。
1件検索でもCollect検索を使う理由を二つ挙げていました。(再掲)
構造を表現する際にCollect検索があるととても楽ができます。
Domaの検索は、基本的にResultSetの1行を、一つのEntityクラスにマッピングします。
従って、n:m の関係だったり 1:0-1 の関係だったりといった複雑なオブジェクト構造を直接マッピングする事ができません。
そこで、Domaでそういった複雑なオブジェクト構造を作る場合には、Entity毎に検索を行い検索結果を手で組み立てます。
Entity毎に検索を行えば n:m の関係を下手に join で扱って limmit/offset する時に困るというのもなくなります。
ただ、単純にrootのEntityを取得し、その一つずつ関連するEntityの検索を行ってしまうと簡単にN+1問題 が発生します。
その時にCollect検索の出番です。
例えば以下の様なゲームのEntityの関係を考えます。
Guild : 1 ------ N : Character Guild : 1 ------ 0-1 : GuildHouse
Guild
とCharacter
とGuildHouse
がそれぞれEntityです。
これを以下のようなクラスにマッピングします。
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その他省略
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);
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);
CharacterDao/findByGuildIds.sql
SELECT *FROM `character`WHERE guild_id IN /*guildIds*/(1, 2) AND deleted_time IS NULL
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);
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()って書きたい );}
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)
に変換されるため安全に!
指定した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))
に書き換えられる。
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 |