前方一致の検索条件とドメインクラス #doma2

Doma 2 で前方一致の検索条件をドメインクラスでどう扱うか考えた話です。

馬鹿正直にドメインクラスを使う

SIerで業務アプリ作ってると検索して結果をグリッド表示する画面とかよく作ると思うんですが、 その際に前方一致の検索条件を扱う事も多いのです。

例えば従業員を検索するときに条件に従業員番号と所属部門番号を使えるとします。 加えて、どちらも前方一致で検索するとします。 この検索を行うDAOメソッドを何も考えずに書くとこうなります。

@Select
List<Emp> select(EmpId empId, DeptId deptId);

ちなみにSQLファイルは雰囲気こんな感じで。

SELECT /*%expand */*
  FROM emp
 WHERE empId LIKE /* empId */'AA%'
   AND deptId LIKE /* deptId */'BB%'

EmpIdは従業員番号を表すドメインクラスです。 検索条件は前方一致、つまり従業員番号の後ろが欠けた中途半端な状態のものが渡される可能性があります。 そういった項目にEmpIdを使用するのはおかしいのではないでしょうか?

Stringを使う

そこで検索条件を汎用的な型であるStringに変更します。

@Select
List<Emp> select(String empId, String deptId);

EmpIdの不適切な使用はなくなりましたが、 select(deptId, empId) というふうに引数の順番を間違えてもコンパイル時に検出されなくなってしまいました。

ドメインクラスPrefix<DOMAIN>を作る

というわけで考えたのがPrefix<DOMAIN>というドメインクラスです。

次にコードを記載します。

import java.util.Optional;

import org.seasar.doma.Domain;

@Domain(valueType = String.class, factoryMethod = "of")
public class Prefix<DOMAIN> {

    private final String value;

    private Prefix(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public static <T> Prefix<T> of(String value) {
        return Optional.ofNullable(value).map(x -> new Prefix<T>(x)).orElse(null);
    }
}

型パラメータDOMAINには他のドメインクラスをバインドします。

このPrefix<DOMAIN>を使用するとDAOメソッドは次のようになります。

@Select
List<Emp> select(Prefix<EmpId> empId, Prefix<DeptId> deptId);

これで、

  • ドメインクラスに適しない前方一致に使用するような中途半端な状態を持てる
  • DAOメソッドの引数の順を間違えてもコンパイル時に検出できる

という思いを実現する事ができました。 現時点ではベターな手法だと思っています。

これはドメインクラスをジェネリックなクラスに出来るが故にとれた手法ですが、 そういった事ができるようになったのは実は私の要望だったりします。

あのときはこんなことに使えるとは露程も思っていませんでした(・ω<) テヘペロ

Doma 2良いよ!

「Javaによる関数型プログラミング」読んだ

さくらばさんに献本して頂きました。 ありがとうございます!

物理的に薄い本ですし、ラムダ式と関数型プログラミングへの入門として良い本だと思います。 チームの後輩に読んで欲しい。

関数型プログラミングへの入門に丁度いいということで、数年前にコップ本で入門を済ませていた私としては少々物足りない気がしました。 ただし7章は末尾再帰の最適化を行うという内容で、そこはJavaコンパイラはサポートしていない部分なので興味深く読みました。 恥ずかしながら、末尾再帰の最適化を自分で書くという発想は無かったので参考になります。

以下、気になった点を挙げます。 タイポも含む。

  • 2〜3ページ。宣言的なコードとはどういうことかを、 いきなりラムダ式を登場させるのではなくJava 7までの語彙で説明しているのが良いですね。

  • 28ページの例2-7。 これメソッド参照になっていないのでコンパイルエラーですね。

  • 70ページ。

    JDKの新しい ClosableStream インターフェース

    とありますが、そのようなクラスはありません。

    リリース前にはあったようですが be6ca7197e0e あたりで削除されました。 ちなみに ClosableStream じゃなくて CloseableStream でした。

  • 89ページの例4-17。 コードを引用します。

    try {
      final URL url =
        new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
    
      final BufferedReader reader =
        new BufferedReader(new InputStreamReader(url.openStream()));
      final String data = reader.lines().skip(1).findFirst().get();
      final String[] dataItems = data.split(",");
      return new BigDecimal(dataItems[dataItems.length - 1]);
    } catch(Exception ex) {
      throw new RuntimeException(ex);
    }
    

    これ、readerがcloseされていません。 readerをtry-with-resourcesで囲むべきと思います。

    try {
      final URL url =
        new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
    
      try(final BufferedReader reader =
          new BufferedReader(new InputStreamReader(url.openStream()))) {
        final String data = reader.lines().skip(1).findFirst().get();
        final String[] dataItems = data.split(",");
        return new BigDecimal(dataItems[dataItems.length - 1]);
      }
    } catch(Exception ex) {
      throw new RuntimeException(ex);
    }
    
  • 130〜135ページのインスタンス化の遅延。 面白いアプローチですが最終的に出来上がったコードは少々分かりにくかったし、synchronizedを使っていたのでConcurrency Utilitiesでもうちょっと良い感じに書けるんじゃ? と思い色々考えた挙げ句、次のようなコードを書いてみましたがたいして分かりやすくなりませんでした_(:3」∠)_

    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    import java.util.concurrent.atomic.AtomicReference;
    
    public class HoderNaive {
    
        private final AtomicReference<FutureTask<Heavy>> heavy = new AtomicReference<>();
    
        public Heavy getHeavy() {
            if (heavy.compareAndSet(null, new FutureTask<>(Heavy::new))) {
                heavy.get().run();
            }
            try {
                return heavy.get().get();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e.getCause());
            }
        }
    }
    
  • 161ページ。

    正しい5の階乗と、120の階乗の一部

    120ではなく20000の階乗ですね。

まとめ

関数型プログラミングにまったく触れたことがない人や若手にJava 8を教える立場にある人にはおすすめです。

コップ本を読んだ程度には関数型プログラミングに触れたことがある人は7章だけ読みましょう。