Android で LINQ を実現!

はじめに

こんにちは。お久しぶりですね。hiroです。

昨今の、オブジェクト指向の考え方の普及に伴い、プログラマの誰もがオブジェクト指向言語を用いてソフトウェアを開発するようになりました。しかし同時にリレーショナルデータベースや、といった、オブジェクト指向では表されていないデータ構造へのアクセスが容易ではない事が課題として挙がっています。

この「Collection Context」ライブラリでは、これらの非オブジェクト指向なデータ構造を持つデータソースを、オブジェクト指向言語であるJava言語でいかに容易に扱うかをテーマに、コレクション操作が容易になるようなを提供しています。

1.JavaにLINQのエッセンスを

  Microsoft社の.NET Frameworkでは、C# 3.0およびVisual Basic 9.0よりサポートされたLINQ式およびLINQ関連ライブラリのAPIセットによってコレクション操作を容易にしています。特にC#、VB言語においては、言語に統合されたクエリ式が記述できるように言語機能が拡張されています。これにより、メモリ上のオブジェクト、データベース上のオブジェクト、XMLなどがほぼ同一のクエリ記法によって操作が可能です。このように、.NET FrameworkはAPIが徐々にモダンに置き換わります。

  一方、Javaにはこのような言語拡張はありません。コレクション関連のAPIも、Java Collection Frameworkがジェネリクスに対応したJ2SE 5.0以降、これといった拡張もありません。また、データベースアクセスはJDBCとして不動、XML操作はStAXがJava 6で加わりましたが、これらを横断して「データの集合」という概念で扱えるフレームワークやライブラリはありません。

  最近、Javaには新しいプラットフォームが増えました。Androidです。Androidで採用されているJavaは、正確にはSun Microsystems社のJavaではありませんが、Java SEの主要なAPIをサポートし、さらにAndroidに特化したAPIを提供しています。このプラットフォーム上においても、コレクション操作に関するライブラリはかつてのJavaのままです。
ということで、「JavaにLINQのエッセンスを」をテーマに、モダンなコレクション操作機能を提供するためのライブラリ、それが「Collection Context」です。

2..NET FrameworkのLINQとの違い

  .NET FrameworkのLINQの実装は、LINQプロバイダと呼ばれるプロバイダによって機能が提供されるようになっています。メモリ上のオブジェクト用LINQプロバイダ、データベース向けLINQプロバイダ、XML向けLINQプロバイダといった調子です。Collection Contextもこの概念に則っています。

  違いは、Javaにクエリ式を記述できる言語拡張が無いという点と、ラムダ式が無いという点です。LINQは専用の文法を使用しても、メソッドをコールしても利用できます。Collection Contextは、Java向けの専用の文法が無いため、メソッドをコールして使用します。

3.Collection Contextのライブラリセットとプラットフォーム

  Collection Contextは単一のライブラリではありません。現在、以下の2種類にて分かれています。
(1)Collection Contextのフレームワーク部分
  Collection Contextの名を表す根幹です。コレクション操作の文脈(処理の流れ)をAPIで提供します。
また、XML文字列を簡単に操作するための、DOMに類似した(ただし、別のアプローチからの)モダンなXML関連APIを提供します。このAPIは各プラットフォームで用意されている標準のパーサを使用するように作られており、パーサ自体は含んでいません。

(2)プラットフォームのAPIによるリソースマネジメントの実装
  実際に、プラットフォームで扱えるリソースをマネジメントするためのAPIが準備されています。2010年10月現在、Androidプラットフォーム向けの実装が準備されており、このプラットフォーム上では以下のリソースがCollection Context上で扱えるようになります。

  1. SQLiteデータベース
  2. JSONオブジェクト
  3. XML(org.xmlpull.v1パッケージに含まれるプル型パーサと、SAXパーサの両方)
  4. メモリ上のオブジェクト

これにより、現在Collection Contextが稼働可能なプラットフォームはAndroid 1.6以降となります。サーバサイドJava開発に必要なライブラリの実装は今後の課題としています。

 

※ダウンロードしてご利用いただけるjarファイルは、このAndroid 1.6以降でご利用頂けるもので、上記(1), (2)のすべてをワンパッケージにしています。

4.CollectionContext<T>クラスの基礎

  まずはじめに、Collection Contextを用いたXML処理の例を以下に示します。これは、AndroidプラットフォームにおいてXMLデータをJavaのオブジェクトに変換しています。

String uri = "http://www.example.com/example-resource";
Iterable<Person> persons = XElement
.from(uri, new XmlSaxParserWrapper(), "UTF-8")
.descendants("person")
.where(new F1<XElement, Boolean>() { public Boolean f(XElement node) {
return node.attribute("person-id").intValue() > 10;
}})
.select(new F1<XElement, Person>() { public Person f(XElement node) {
Person person = new Person();
person.setID(node.attribute("id").longValue());
person.setName(node.element("name").stringValue());
person.setAddress(node.element("address").stringValue());
person.setBirthday(node.element("birthday").dateTimeValue("yyyy-MM-dd"));
return person;
}});

  XElementクラスのstaticメソッドfrom()に、XMLのURIと、XMLパーサのインスタンス、XMLの文字コードを指定し、XMLをパースする方法を指定するところから記述が始まっています。次のdesendants()メソッドは、ルートタグの次の階層の「person」という名称のタグ情報をコレクション化するメソッドです。 そのコレクションにwhere()メソッドにてフィルタ条件を指定しています。少し見にくいかもしれませんが、関数を表すオブジェクトが匿名クラスとして実装されています。さらに、select()メソッドにて射影操作を指定しています。XElementのオブジェクトであるnodeをPersonクラスのオブジェクトに変換しています。select()メソッドが返すインスタンスはjava.lang.Iterable型を実装しています。従ってpersonsオブジェクトは拡張for文で逐次参照が可能です。

  このように、Collection Contextフレームワークを用いると、ネットワーク処理とXMLパース処理における各種リソースのマネジメントが自動化されます。これによって、プログラマはデータの変換処理を容易に記述でき、この処理の前後のビジネスロジックの記述に専念できます。

  上記where()メソッドやselect()メソッドはCollectionContext<T>クラスのメソッドです。CollectionContext<T>クラスは、Collection Contextフレームワークの中核をなすクラスで、配列やIterable<S>を実装するオブジェクトに対するフィルタ処理や射影処理等をメソッドチェーン方式で記述できる機能を提供します。

CollectionContext<T>クラスにもstaticなfrom()メソッドが用意されており、配列やIterable<T>を実装するオブジェクトをCollectionContext<T>オブジェクトに変換することができます。

CollectionContext<T>クラスで指定できる機能とメソッドは以下の通りです。

 

  1. from():既存の配列やIterable<T>で表されるコレクションをCollectionContext<T>オブジェクトに変換する。
  2. where():フィルタを指定する。指定されたオブジェクトは遅延実行される。
  3. select():射影を指定する。このメソッドで返されるCollectionContextは、射影後のコレクションを表す。遅延実行される。
  4. comparator()、orderByAsc()、orderByDesc():ソート条件を指定する。遅延実行される。
  5. foreach():コレクションに上記フィルタ条件、ソート条件を適用しながら1件毎に、指定された処理ルーチンをコールする。メソッドが呼ばれた時点で即時実行される。
  6. first():コレクションにフィルタ条件、ソート条件を適用し、最初の1件を取得する。オーバーライドされたメソッドに、射影を行うことができるタイプもある。即時実行される。
  7. appendTo():既存のList<T>オブジェクトで表されるコレクションの末尾に、このCollectionContext<T>オブジェクトで表されるコレクションをコピーする。フィルタおよびソート条件が適用される。即時実行される。
  8. toList():内包するコレクションにフィルタおよびソート条件が適用され、List<T>オブジェクトを生成する。即時実行される。

  つまり、CollectionContext<T>クラスのみを用いてコレクション操作を行う場合、from()メソッドの取りうるタイプの都合上、メモリ上のコレクションか、独自にIterable<S>を実装したクラスのオブジェクトを指定する必要があります。逆に、このインターフェイスに則ったクラスを作成することで、Collection Contextフレームワークがサポートしていないリソース経由でデータを取得し、コレクション操作を行うことも可能です。

 

  また、CollectionContext<T>クラスは、Iterable<T>インターフェイスを実装しています。従って、このクラスのオブジェクトは直接、拡張for文で使用できます。

5.CollectionContext<T>オブジェクトを生成できる各クラス

  CollectionContext<T>オブジェクトを生成できるのは、自身のfrom()メソッドだけではありません。上記(1)の末尾でも述べられているように、Iterable<S>を実装したクラスのオブジェクトを生成できるクラスも以下のように提供されています。ただし、2010年10月現在、これらが稼働するのはAndroidプラットフォームの、バージョン1.6以上のみです。

(1)XML中のデータをコレクション化するXElementクラス
サンプルコードは既出のとおりです。

(2)SQLiteデータベースの参照処理を生成するSQLiteContextBuilderクラス

  Androidプラットフォームには、組込データベースとして、SQLiteが採用されており、プラットフォームの標準APIとしても実装されています。ただし、このAPIはJDBCではありません。独自に実装されたAPIとなっています。

Collection Contextフレームワークでは、SQLite関連クラスとして、DatabaseCursorIterableクラスと、SQLiteContextBuilderクラスが標準で提供されています。DatabaseCursorIterableはIterableを実装しているため、CollectionContext<T>クラスのfrom()メソッドで使用できます。しかし、通常はSQLiteContextBuilderを使用し、メソッドチェーンの記法を用いて「簡単に」データベースの参照コードを記述します。これにより、コードがより宣言型に近づき、バグも潜みにくくなります。

CollectionContext<Shop> shops = SQLiteContextBuilder
.newBuilder(new SQLiteOpenHelperImpl())
.query(   "SELECT " +
"  ID, " +
"  SHOP_ID, " +
"  NAME, " +
"  LATITUDE, " +
"  LONGITUDE, " +
"  URL " +
"FROM " +
"  SHOP " +
"WHERE " +
"  ? <= REGISTERED AND " +
"  REGISTERED < ? "
registeredBegin, registeredEnd)
.contextToRead()
.select(new F1<Cursor, Shop>() { public Shop f(Cursor cursor) {
Shop result = new Shop();
result.id = cursor.getInt(0);
result.shopID = cursor.getInt(1);
result.name   = cursor.getString(2);
result.latitude   = cursor.getDouble(3);
result.longitude    = cursor.getDouble(4);
result.url  = cursor.getString(5);
return result;
}});

  このコードでは、query()メソッドでSQL文とパラメータを指定しています。contextToRead()メソッドをコールすると、それ以降はCollectionContextクラスのメソッドとなります。select()メソッドを使い、CursorオブジェクトからShopオブジェクトへ射影しています。最終的に得られる値はCollectionContextオブジェクトです。

(3)JSON文字列を処理するorg.json.JSONObjectクラスをコレクションとみなすJSONObjectIterableクラス

  最近は、Web上のAPIのデータ送受信の表現として、JSONを用いることも多くなってきました。Facebookは特に、全般的にJSONでデータをやり取りし、ソーシャルグラフを操作します。AndroidプラットフォームにもJSONを操作するAPIが用意されています。これをCollectionContext<T>オブジェクトに変換している例が以下の例です。

String result = facebook.request(url, parameters, method);
CollectionContext<FacebookFriend> friends = JSONObjectIterable
.from(result, "data")
.select(new F1<JSONObject, FacebookFriend>() {
public FacebookFriend f(JSONObject friend) {
FacebookFriend result = new FacebookFriend();
result.facebookID = friend.optString("id");
result.name   = friend.optString("name");
return result;
}});

  「友達」を表すJSONObject “friend”を、FacebookFriendクラスのオブジェクトに変換しています。JSONObjectIterableクラスのfrom()メソッドは、JSON文字列をJSONObjectに変換する処理を行い、CollectionContextを生成します。そして、select()メソッドで射影を行い、得られるのはCollectionContextオブジェクトです。

(4)内部イテレータの表現を外部イテレータへ変換するOuterIterable<T>クラス

  OuterIterable<T>クラスは特殊用途を想定しています。通常、CollectionContext<T>オブジェクトを生成するためには、from()メソッドにIterable<T>オブジェクトを渡すのですが、場合によってはIterable<T>を実装したクラスを作成しなければならない場合があります。この実装が外部イテレータの表現を強いられるため、扱うリソースによっては実装が容易でないケースもあります。

例えば、ストリームから文字列を行単位で入力し、その文字列を1行単位で参照できるIterableを実装すると、以下のようになります。

public class OuterIterableImpl
implements Iterable<String>, Iterator<String> {
private boolean initialized = false;
private String filePath = null;
private BufferedReader reader = null;
private String next = null;
public OuterIterableImpl1(String filePath) {
this.filePath = filePath;
}
@Override
public Iterator<String> iterator() {
return this;
}
private void initialize() throws IOException {
this.reader =
new BufferedReader(new InputStreamReader(
new FileInputStream(this.filePath)));
}
private void dispose() {
try {
this.reader.close();
}
catch (IOException ex) {
// something to do
}
}
@Override
public boolean hasNext() {
if (!this.initialized) {
try {
initialize();
}
catch (IOException ex) {
return false;
}
}
try {
String nextLine = this.reader.readLine();
if (nextLine != null) {
this.next = nextLine;
return true;
}
else {
dispose();
return false;
}
}
catch (IOException ex) {
dispose();
return false;
}
}
@Override
public String next() {
return this.next;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}

  非常に長い上、ストリームというリソースを扱うライフサイクルがメソッド間で分離され、メンテナンスが容易でなくなります。一方、context.OuterIterable<T>を使用してIterableを実装すると以下のようになります。

public class OuterIterableImpl2 extends OuterIterable<String> {
private String filePath = null;
public OuterIterableImpl2(String filePath) {
this.filePath = filePath;
}
@Override
protected void iterate() {
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(this.filePath)));
}
catch (IOException ex) {
return;
}
try {
String nextLine = reader.readLine();
while (nextLine != null) {
yield(nextLine);
}
}
catch (IOException ex) {
// something to do
}
finally {
try { reader.close(); } catch (IOException ex) { }
}
}
}

特徴はyield()メソッドにあります。このメソッドへ文字列を指定することで、このIterableオブジェクトは指定された順番で文字列を取り出すことができます。このOuterIterable<T>オブジェクトをCollectionContext<T>クラスのfrom()メソッドに指定することで、ストリームから得た文字列のコレクションにフィルタをかけたり射影したりできるようになります。

6.Collection Contextフレームワークのご利用にあたって

このCollection Contextフレームワークは、当社実験サイト「YUMEMI Labs」からダウンロードしてご利用頂けます。ただし、自己責任でご利用ください。当社はいかなる損害も補償できませんのであしからずご了承ください。

初期リリースはバイナリのみの配布とさせていただきます。オープンソース化は今後検討いたします。

開発用ドキュメントは、この「ゆめ技」の記事と、javadocです。ご意見・ご要望・ご質問のすべてにお答えできるかどうかは分かりませんが、皆さんのご意見・ご要望がこのフレームワークの成長を加速させる原動力であることは確かです。また、頂いたご質問を元に、ゆめ技の記事を書いていこうと考えています。どうぞよろしくお願いします。

■アプリケーション開発と実行に必要なライブラリファイルのダウンロードCollectionContextAndroid1.0.0.jar

■アプリケーションの開発時に参照するドキュメントファイルをダウンロードCollectionContextAndroid1.0.0_javadoc.zip


Comments are closed.