Smart Object Query(Java用LINQ to Objectsモドキ私家版)公開

昨年末の気分は既に長期休暇モードな時期に、手慰みで作っていたライブラリを公開してみました(・∀・)
https://github.com/usausa/Java-Smart-ObjectQuery

なんぞこれ(・ω・)?

.NETもJavaも仕事でやっている人間なら一度は実装を試みる、LINQ(to Objects)っぽい感じのコレクション操作ライブラリの私家版です。
実用性はおいといて、LINQ(to Objects)ってどういうものよ?、っというのをJavaなりの解釈で実装するとどうなるか、っということをやってみた感じです(・ω・) *1

そもそも

そもそもLINQ to Objectsって使うの(・ω・)?、っという話がありますが。
こういう意見が出る背景としては、

  • 実行効率を考えればクエリはエッジ(データストア、もっというとRDB内)でやるべきものじゃないの(・ω・)?

っというものがあると思います。
勿論、それは当然とした上で、

  • データストア上のデータ構造と、プレゼンテーション層用のデータ構造は一致しない

みたいな話もあるわけで。


例えばRDBのようなRowセットに対して、画面も単純な表形式であれば単純に処理を記述できますが。
オサレなデザインや分析系の画面などで、動的な縦横変換やデータ間の関係を考慮した描画が必要になったり、そこに権限とかその他要素諸々が絡んできたりすると、データストア上のデータ構造そのままを使って描画処理を記述しようとすると、処理の記述が煩雑になってきたりします(・ω・;)
そこで、データストア上のデータ構造をプレゼンテーション層用のデータ構造に変換して、描画処理の記述を簡潔にしようとしたりするわけで、自分は、この手の処理にLINQ(to Object)をよく使用します(・∀・)*2


あと、データストア上でのクエリをこねくり回して、プレゼンテーション層用のデータ構造でデータを取得することも出来るんじゃないの(・ω・)?、っという話もありますが。
それは責務が違うし、プレゼンテーションが複雑になればなるほど破綻するので、自分としては、

  • データストアからはデータストア上のデータ構造のまま、最適なプランでデータを取得する
  • アプリケーション内でデータストア上のデータ構造をプレゼンテーション層用のデータ構造に変換
  • プレゼンテーションではプレゼンテーション層用のデータ構造を用いて簡潔に処理を記述する

っというパターン押しです(`・ω・´)

本題/使い方

っで、Smart Object Queryについてですが、テストコードもちゃんと用意していないので、ここに少し使い方を書いておきます。

処理一覧
メソッド 分類 種別 Iterator
fromArray クエリ化 始点 (ArrayList)
from 始点 (元のIterator)
fromEnumeration 始点 EnumerationIterator
fromEmpty 始点 EmptyIterator
fromSingle 始点 SingleIterator
repeat 集合生成 始点 RepeatIterator
range 始点 RangeIterator
rownum インデックス付加 中途 IndexedIterator
defaultIfEmpty 空集合 中途 DefaultIfEmptyIterator
conact 集合演算 結合 ConactIterator
distinct 中途 DistinctIterator
union 結合 UnionIterator
expect 結合 ExpectIterator
intersect 結合 IntersectIterator
zip 平行アクセス 結合 ZipIterator
skip ページング 中途 SkipIterator
skipWhile 中途 SkipWhileIterator
take 中途 TakeIterator
takeWhile 中途 TakeWhileIterator
order ソート 再評価 (ArrayList)
reverse 再評価 (ArrayList)
all 限量詞演算 終端 -
any 終端 -
contains 終端 -
first 取得 終端 -
last 終端 -
elementAt 終端 -
count 集約演算 終端 -
aggregate 終端 -
sum 終端 -
max 終端 -
min 終端 -
average 終端 -
where 選択 中途 WhereIterator
groupBy グループ化 中途 GroupedIterator
select 射影 中途 SelectIterator
selectMany 中途 SelectManyIterator
join 結合 結合 JoinIterator
groupJoin 結合 GroupJoinIterator
toList 変換 終端 -
toSet 終端 -
toMap 終端 -
toLookup 終端 -
each 評価 副作用 -

メソッド」はObjectQueryクラスのメソッドです。
引数のSelector等の違いによる、複数定義があるものもあります。


「分類」はざっくりとした分類です(・∀・;)


「種別」は、そのメソッドIteratorのチェインの始点/途中/終端のどれかについての分類です。


Iterator」はその処理を実装するIteratorです。
(ArrayList)のような記述は、そのクラスを利用して処理を実装しているものです。

基本

基本ということで、Listに入った0〜9の数値のうち、偶数だけを取得するような処理の記述

List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; i++) {
    list.add(i);
}

for (Integer value : ObjectQuery.from(list).where(new Predicate<Integer>() {
    @Override
    public boolean test(final Integer param) {
        return param % 2 == 0;
    }})) {
    System.out.println(value);
}
[実行結果]
0
2
4
6
8

ラムダが無いとPredicateの記述が冗長ですよね。
っということで、よく使用する関数やセレクタは、専用のクラスへ定義を集約したり、Beanのstaticメンバなんかに定義しておくのもありだと思います。

public class Functions {

    public static final Predicate<Integer> EVEN = new Predicate<Integer>() {
        @Override
        public boolean test(final Integer param) {
            return param % 2 == 0;
        }
    };
}

を用意しておいて、

for (Integer value : ObjectQuery.from(list).where(Functions.EVEN)) {
    System.out.println(value);
}

とか。

ページング

次のサンプルはページングということで、まずデータ用のクラスとして下記のようなものを用意して。

public class Data {

    private int id;

    private String data;

    public int getId() {
        return id;
    }

    public String getData() {
        return data;
    }

    public Data(final int id, final String data) {
        this.id = id;
        this.data = data;
    }

    @Override
    public String toString() {
        return "{ id = [" + id + "], data = [" + data + "] }";
    }
}

ついでにデータの作成もObjectQueryを使ってみたりして。

Iterable<Data> datas = ObjectQuery.range(1, 10).select(new Func1<Integer, Data>() {
    @Override
    public Data eval(final Integer id) {
        return new Data(id, "Data-" + id);
    }
});

for (Data data : datas) {
    System.out.println(data);
}
[実行結果]
{ id = [1], data = [Data-1] }
{ id = [2], data = [Data-2] }
{ id = [3], data = [Data-3] }
{ id = [4], data = [Data-4] }
{ id = [5], data = [Data-5] }
{ id = [6], data = [Data-6] }
{ id = [7], data = [Data-7] }
{ id = [8], data = [Data-8] }
{ id = [9], data = [Data-9] }
{ id = [10], data = [Data-10] }

あと、表示についてはeach()メソッドを使用してこんな風にも書けたりとか。

ObjectQuery.from(datas).each(new Action<Data>() {
    @Override
    public void run(final Data data) {
        System.out.println(data);
    }
});

っで、本題のページングですが、

ObjectQuery
    .from(datas)                            // datasから
    .order(new Func1<Data, Integer>() {     // id逆順でソートして
        @Override
        public Integer eval(final Data data) {
            return - data.getId();
        }
    })
    .skip(5)                               // 5件飛ばしから
    .take(2)                               // 2件取得したものを
    .each(new Action<Data>() {             // 表示
        @Override
        public void run(final Data data) {
            System.out.println(data);
        }
    });
[実行結果]
{ id = [5], data = [Data-5] }
{ id = [4], data = [Data-4] }

みたいな感じで。

取得

条件にマッチする最初のデータを取得。

Data data = ObjectQuery.from(datas).first(new Predicate<Data>() {
    @Override
    public boolean test(final Data data) {
        return data.getId() % 2 == 1;
    }
});
System.out.println(data);
[実行結果]
{ id = [1], data = [Data-1] }

条件にマッチする最後のデータを取得。

Data data = ObjectQuery.from(datas).last(new Predicate<Data>() {
    @Override
    public boolean test(final Data data) {
        return data.getId() % 2 == 1;
    }
});
System.out.println(data);
[実行結果]
{ id = [9], data = [Data-9] }

first()、last()メソッドは、本家におけるFirstOrDefault()、LastOrDefault()に対応するもので、データが無い時に例外を投げるバージョンは用意していません。
なぜなら、自分がそのパターンをあまり使わないから(・∀・;)
関連して、single()も用意していなかったり。

判定処理

コレクション中の値について、全ての値が条件を満たすかチェックするall()と、条件を満たすものが一つでもあるかチェックするany()について。
便宜上下記のようなメソッドを用意しておいて。

public static class Functions {

    public static final Predicate<Integer> lessThan(final int compare) {
        return new Predicate<Integer>() {
            @Override
            public boolean test(final Integer value) {
                return value < compare;
            }
        };
    }

    public static final Predicate<Integer> greaterThan(final int compare) {
        return new Predicate<Integer>() {
            @Override
            public boolean test(final Integer value) {
                return value < compare;
            }
        };
    }
}

要素全てが条件を満たす判定処理all()と、要素の何れかが条件を満たす判定処理any()の使用方法は下記のような感じで。

// 1..10は全て11未満
assertTrue(ObjectQuery.range(1, 10).all(Functions.lessThan(11)));
// 1..10は全て10未満では無い
assertFalse(ObjectQuery.range(1, 10).all(Functions.lessThan(10)));

// 1..10は何れかが9より大きい
assertTrue(ObjectQuery.range(1, 10).any(Functions.greaterThan(9)));
// 1..10は何れかが10より大きくない
assertFalse(ObjectQuery.range(1, 10).any(Functions.greaterThan(10)));
集計関数

件数を取得するcount()、最大値max()、最小値min()、平均値average()、汎用/内部的に使用しているaggregate()とか。
Javaの都合上、戻り値の型毎にmaxLong()、maxDouble()等のバリエーションを用意(・ω・;)


っで、使い方の前に、Selectorの関数を以下のように用意しておいて。

public class Data {

    private int id;
...
    public static Func1<Data, Integer> ID_SELECTOR = new Func1<Data, Integer>() {
        @Override
        public Integer eval(final Data data) {
            return data.getId();
        }
    };
}

以下、使用方法。

int count = ObjectQuery.from(datas).count(new Predicate<Data>() {
    @Override
    public boolean test(final Data data) {
        return data.getId() > 5;
    }
});
System.out.println(count);
int max = ObjectQuery.from(datas).max(Data.ID_SELECTOR);
System.out.println(max);
int min = ObjectQuery.from(datas).min(Data.ID_SELECTOR);
System.out.println(min);
[実行結果]
5
10
1

なお、int配列に対する処理とかは、ObjectQueryではなくIntegersクラスとかに実装するような方針なのです(・ω・)

集合演算

集合演算(和、積、差)について。
使用するデータは下記だとして。

List<Integer> list = Arrays.asList(1, 3, 2, 3);

List<Integer> list1 = Arrays.asList(1, 3, 2);
List<Integer> list2 = Arrays.asList(3, 4);

重複削除。

for (Integer value : ObjectQuery.from(list).distinct()) {
    System.out.println(value);
}
[実行結果]
1
3
2

和。

for (Integer value : ObjectQuery.from(list1).union(list2)) {
    System.out.println(value);
}
[実行結果]
1
3
2
4

積。

for (Integer value : ObjectQuery.from(list1).intersect(list2)) {
    System.out.println(value);
}
[実行結果]
3

差。

for (Integer value : ObjectQuery.from(list1).expect(list2)) {
    System.out.println(value);
}
[実行結果]
1
2
結合

次は結合処理について(・ω・)
まず、以下のようなデータクラス(トランザクション系およびマスタ系だと思いねえ)を用意。

public class TxData {

    private String id;

    private int masterId;

    public String getId() {
        return id;
    }

    public int getMasterId() {
        return masterId;
    }

    public TxData(String id, int masterId) {
        this.id = id;
        this.masterId = masterId;
    }

    public static final Func1<TxData, Integer> MASTER_ID_SELECTOR = new Func1<TxData, Integer>() {
        @Override
        public Integer eval(final TxData data) {
            return data.getMasterId();
        }
    };
}
public class MasterData {

    private int id;

    private String name;

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public MasterData(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public static final Func1<MasterData, Integer> ID_SELECTOR = new Func1<MasterData, Integer>() {
        @Override
        public Integer eval(final MasterData data) {
            return data.getId();
        }
    };
}

あと、ヘルパーとして下記の様なTuple作成処理も用意しておいて。

public static class Functions {

    public static <T1, T2> Func2<T1, T2, Tuple2<T1, T2>> makeTuple() {
        return new Func2<T1, T2, Tuple2<T1,T2>>() {
            @Override
            public Tuple2<T1, T2> eval(final T1 value1, final T2 value2) {
                return Tuple2.pair(value1, value2);
            }
        };
    }
}

っで、テストデータとしては以下のようなものを用意するとして。

List<TxData> txs = new ArrayList<TxData>();
txs.add(new TxData("A", 1));
txs.add(new TxData("B", 2));
txs.add(new TxData("C", 2));
txs.add(new TxData("D", 4));
txs.add(new TxData("E", 4));
txs.add(new TxData("F", 4));

List<MasterData> masters = new ArrayList<MasterData>();
masters.add(new MasterData(1, "Data-1"));
masters.add(new MasterData(2, "Data-2"));
masters.add(new MasterData(3, "Data-3"));
masters.add(new MasterData(4, "Data-4"));
masters.add(new MasterData(5, "Data-5"));

まずjoin()。

for (Tuple2<TxData, MasterData> tuple : ObjectQuery.from(txs).join(masters, TxData.MASTER_ID_SELECTOR, MasterData.ID_SELECTOR, Functions.<TxData, MasterData>makeTuple())) {
    System.out.println(tuple.getValue1().getId() + ":" + tuple.getValue2().getName() );
}
[実行結果]
A:Data-1
B:Data-2
C:Data-2
D:Data-4
E:Data-4
F:Data-4

次にgroupJoin()。

for (Tuple2<MasterData, Iterable<TxData>> tuple : ObjectQuery.from(masters).groupJoin(txs, MasterData.ID_SELECTOR, TxData.MASTER_ID_SELECTOR, Functions.<MasterData, Iterable<TxData>>makeTuple())) {
    System.out.println(tuple.getValue1().getId() + ":" + ObjectQuery.from(tuple.getValue2()).count());
}
[実行結果]
1:1
2:2
3:0
4:3
5:0
変換

っで、最後はデータストア上のデータ構造とプレゼンテーション層用のデータ構造の変換するようなパターンについて。
ここではLookup(Java版はLinkedHashMapを使った簡易実装)とObjectQuery独自のrownum()を使用してchop()処理を作成するサンプル。
chopっていうのは、Listを指定件数ごとに分割したList>みたいな形に変換する処理のこと。
rownum()は、本家LINQにおいて、いくつかのメソッドでFuncを引数にした定義があるものの代わりです。
データ構造Tを、Tと連番をタプルにしたデータ構造Indexedに変換する処理(Iterator)ですだ(・ω・)


まず、chop()の実装は以下のような感じで。

public static <T> Lookup<Integer, T> chop(final Iterable<T> collection, final int size) {
    return ObjectQuery.from(collection).rownum().toLookup(new Func1<Indexed<T>, Integer>() {
        @Override
        public Integer eval(final Indexed<T> param) {
            return param.getIndex() / size;
        }
    }, new Func1<Indexed<T>, T>() {
        @Override
        public T eval(final Indexed<T> param) {
            return param.getElement();
        }
    });
}

これを使って、12件のデータを5件毎に分割する処理を記述すると、以下のような感じになります。

Iterable<String> list = ObjectQuery.range(1, 12).select(new Func1<Integer, String>() {
    @Override
    public String eval(final Integer value) {
        return "Data-" + value;
    }
});

for (Grouping<Integer, String> grouping : chop(list, 5)) {
    System.out.println(grouping.getKey());
    for (String data : grouping) {
        System.out.println("  " + data);
    }
}
[実行結果]
0
  Data-1
  Data-2
  Data-3
  Data-4
  Data-5
1
  Data-6
  Data-7
  Data-8
  Data-9
  Data-10
2
  Data-11
  Data-12

RDBから取得したRowセットのデータ構造を、この種の変換でプレゼンテーション用の構造に変換すれば、jspでの記述は単純になりますよね、っという話ですだ(`・ω・´)

中身についても少し、Iterator!、Iterator!!、Iterator!!!

ObjectQueryは、LINQ(to Objects)モドキのライブラリの中でも、シンプルでベタな実装だと思います(・∀・;)
っというか、本家LINQ(to Objects)が何者か*3について、Javaプログラマにも理解してもらうことを意識して実装したものです。


ただ、Javaの言語仕様的に苦しい点もいくつかあるわけですが(・ω・;)

拡張メソッドが無い点

本家LINQ(to Objects)はIEnumerableに対する拡張メソッドとして実装されているわけで、拡張メソッドがあれば、staticなメソッドを用意するだけでFluentな記述ができますが。
LINQモドキの中には、Fluentな記述ではなくstatic importを使用して下記のような記述にしているものもあったりして。

select(where(list, predicate), selector);

一方のObjectQueryでは、IterableをラッピングしたしたObjectQueryインスタンスに変換することで、メソッドチェーンを使用した記述ができるようにしています。
そして、ObjectQuery自体がIterableをimplementsすることで、そのまま拡張forループに渡せるようにしています。
まあ、この辺は趣味の問題かしら(・ω・)?

yieldが無い点、currentとnextの違い

各種Iteratorについては、yieldが無いので状態を保存するためのメンバを用意したりとわかりにくい実装になっているものもあります。


そして以外と厄介なのが、本家におけるIEnumeratorがCurrentを処理するものなのに対して、JavaIteratorはnextを処理するものだという点。
どこら辺が厄介かと言えば、Iteratorのチェインに対して単純に次のIteratorのnextを処理すれば良いというわけではなく、Predicateでの判定結果も考慮しないといけないという所。
その為、一部IteratorのhasNext()ではプリフェッチに関連してループがより分かりにくいものになっていたり、hasNext()の複数回呼び出しを考慮して次の値をキャッシュするようになっていたりします。
この辺、自分がよく考えないで実装しているだけで、もっと綺麗に書けるかも(´・ω・`)

ラムダが無い点、Genericsの扱い、functorについて

ラムダが無い点について、利用者から見た場合のfunctorを匿名クラスで記述するので冗長になる点については、サンプルにあるようなFunctionsクラスを用意したり、selectorをstaticメンバとして保持することで若干の緩和は出来ますが(・∀・;)


ObjectQuery実装内の話については、Genericsの扱いの違いから、functorクラスの使用方法が本家とは違う考えになっているものがあります。
例えば、本家ではPredicateではなくFuncな点については、JavaだとBooleanにしかできないので、nullが返されないようにFuncではなく別途Predicateを用意していたりとか(・ω・)
Funcを引数に取るメソッドの代わりにrownum()を用意しているのも似たような理由だったりして。



っということで、自作ライブラリ(実用というより学習用)の紹介日記でした(・ω・)

*1:ちゃんとしたのをお探しなら、Quaereとかlambdajとかをドゾ(・∀・)

*2:逆に、それ以外の用途ではあまり使用しないかも(・ω・)?

*3:Iterator!、Iterator!!、Iterator!!!のチェーン(・∀・)