Doma2 の1件検索でスカラ値を取る場合に、ちょっと悩んだこと

Doma2 の1件検索はこちらを
検索 — Doma 2.0 ドキュメント 1件検索

悩んだのはこんなケース。
DB から 1行1項目(スカラ値)を取りたいけど、その項目は null の場合もあるし、そもそも条件にマッチする行が無い場合もある。
行が無い場合と、行はあるけど null の場合で、後続の処理が変わるというケース。

Doma の 1件検索のシグネチャはこんな感じ。

@Select
Optional<String> getHoge(int id);

これだと 行がありデータも null でない時は良いけど、行が無い or データが null の時は、Optional#empty になってしまい判断出来ない。

これを回避するには幾つか方法があって、

  • 戻り値の String を Wrap した DTO を作ってそれで受ける
  • SelectOptions 使って件数取る
  • ensureResult=true でデータ無い時は例外を投げる

1個目はこんなの。

@Data
@Entity
class WrapDto {
  @Column(name="HOGE")
  private final String value;
  public WrapDto(String value) {
    this.value = value;
  }
}
// Dao はこんな感じ
@Select
Optional<WrapDto> getHoge(int id);

WrapDto の field は Optional でも良いかも。 これなら、データが無いときは、getHoge の戻り値が empty。
データがあるけど、null の場合は、WrapDto の field が null*1で、判断出来る。

追記:
DTO を Domain クラスにしたら?という案が出てきましたが、Domain クラスの value はデフォルトでは not null のはずなので、今回の用途にはあわないかなと思います。
ドメインクラス — Doma 2.0 ドキュメント
ドキュメントにはそれらしい事載ってませんがソースで確認出来ます。
doma/Domain.java at master · domaframework/doma · GitHub
@Domain の acceptNull を true にすると null OK になるはず。*2

2個目はこんなの。

@Select
Optional<String> getHoge(int id, SelectOptions options);
// 使う方はこんな感じになる。
SelectOptions options = SelectOptions.get().count();
Optional<String> op = dao.getHoge(1, options);
if (options.getCount() == 0) {
  // データが無いとき
} else if (op.isPresent()) {
  // データがあって null で無い時
} else {
  // データがあるけど null の時
}

検索 — Doma 2.0 ドキュメント 集計
SelectOptions#count は 今のところクエリを2回投げる形になってるのでちょっと使いづらい感じ。
Doma が内部でこんな感じにクエリ書き換えてくれたら1回で済みそうだけど DB やバージョンの差異とか考えると面倒そう。

select 
 ...
 , count(*) over() as __domaselectoptionscount 
from 
 ...
where
 ...
order by 
 ...

3個目はこんなの

@Select(ensureResult=true)
Optional<String> getHoge(int id);
// 使う方はこんな感じになる。
try {
  Optional<String> op = dao.getHoge(1);
  if (op.isPresent()) {
    // データがあって null で無い時
  } else {
    // データがあるけど null の時
  }
} catch (NoResultException _) {
  // データが無かった時
}

検索 — Doma 2.0 ドキュメント 検索結果の保証
catch するのが嫌なのと処理がちょっと分かれるのもあれげな感。
処理を続けて書くならこんなの?

Optoinal<String>> result;
try {
  result = Pair.of(true, dao.getHoge(1));
} catch (NoResultException _) {
  result = null;
}
if (result != null)) {
  if (result.getRight().isPresent()) {
    // データがあって null で無い時
  } else {
    // データがあるけど null の時
  }
} else {
  // データが無い時
}

Optional に null 入れるのは何かやだなー。
久々の xtend ならもうちょいすっきり

val result = try {
  dao.getHoge(1)
} catch (NoResultException _) {
  null
}
switch result {
  case null: // データが無いとき
  case Optional.empty: // データがあるけど null のとき
  default: // データがあるとき
}

何個か書いたけど 1番目の DTO 作るのが良い気がしてます。

*1:Optional にしたら empty

*2:試してませんが