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
まず、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自体がIterableをimplementsすることで、そのまま拡張forループに渡せるようにしています。
まあ、この辺は趣味の問題かしら(・ω・)?
yieldが無い点、currentとnextの違い
各種Iteratorについては、yieldが無いので状態を保存するためのメンバを用意したりとわかりにくい実装になっているものもあります。
そして以外と厄介なのが、本家におけるIEnumeratorがCurrentを処理するものなのに対して、JavaのIteratorはnextを処理するものだという点。
どこら辺が厄介かと言えば、Iteratorのチェインに対して単純に次のIteratorのnextを処理すれば良いというわけではなく、Predicateでの判定結果も考慮しないといけないという所。
その為、一部IteratorのhasNext()ではプリフェッチに関連してループがより分かりにくいものになっていたり、hasNext()の複数回呼び出しを考慮して次の値をキャッシュするようになっていたりします。
この辺、自分がよく考えないで実装しているだけで、もっと綺麗に書けるかも(´・ω・`)
ラムダが無い点、Genericsの扱い、functorについて
ラムダが無い点について、利用者から見た場合のfunctorを匿名クラスで記述するので冗長になる点については、サンプルにあるようなFunctionsクラスを用意したり、selectorをstaticメンバとして保持することで若干の緩和は出来ますが(・∀・;)
ObjectQuery実装内の話については、Genericsの扱いの違いから、functorクラスの使用方法が本家とは違う考えになっているものがあります。
例えば、本家ではPredicate
Func
っということで、自作ライブラリ(実用というより学習用)の紹介日記でした(・ω・)