hermapi

This WordPress.com site is the cat’s pajamas

カテゴリーアーカイブ: Android

ContentResolver.query() の裏技

土日を遊びほうけてしまい、青くなりながら白んできた空を見つつ仕事を始めた pon です。 おはようございます。

さて、今回はちょっと異色の話題です。

携帯電話などで使用されている Android OS について。

今までこのブログで扱ってきた内容とはかけ離れていますが、ちょっと思いついたのと、あまり他の人がやっているのを見た事が無いので紹介します。


初期の Android OS から、プラットフォームには SQLite のデータベース機能が搭載されていました。 これはアプリケーションが少ないメモリで大量のデータを扱う時に必須のテクノロジです。 また、SQLite のプラットフォームラッパとして、ContentResolver という機能も提供されています。 Android は実行環境の中で、住所録やら写真のリストやら、なんやかやとデータベースに登録しておき、アプリケーションが必要とした時にいちいち自力でファイルを探したり、プラットフォーム固有のデータベース内部を解析したりといった作業を代行してくれます。

これはとてもスマートな方法です。 アプリケーションは、実際にプラットフォームがどのような実装でデータを保持しているかについて、実は興味がありません。 必要なデータが取得できればそれでいいのです。 内部でどのようなチューニングがなされていても、そこから答えを導き出す方法が変わらなければ、何をやってくれても別に問題ないんですね。

この方法を使うと、Android OS の内部でさらに高速な実装に変更したり、極端な話 SQLite ではない別のデータベースに差し替わっていても、アプリケーションは気にする必要がありません。

このような抽象化レイヤを挟むと通常はパフォーマンスが低下し、柔軟性が犠牲になりますが、ContentResolver はとてもシンプルで薄いラッパのため、これらの悪影響をほとんど受けないように注意深く作られています。

ここまでは背景のお話です。


ContentResolver の最もよく利用するであろうメソッドは、

Cursor android.content.ContentResolver.query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder )

だと思います。

これは、データベースを URI 形式で識別し、取得したい列名、取得したい行を抽出する条件式、その条件式にインジェクションされるパラメタ、結果をソートする場合の式からなります。

大雑把にいえば、 select “projection” from “uri” [where (“selection” <- “selectionArgs”)] [order by “sortOrder”] と言うクエリを SQLite の実装に投げつけて Cursor を得るお便利メソッドなわけです。

さて、そのお便利メソッドですが、構文をもう一度見て下さい。 一般的な RDB を使用している方なら、この単純な select 文で取れないデータがたくさんある事がお分かりでしょう。 例えば他のテーブルと join して結果を交差させることはできません。 take や skip を使用して、ソート済み結果から一部分を取得する事ができません。 他にも色々できません。

projection の中には (“列名” + 1) as _count とか、ちょっとした式を記述する事ができます。 selection でも、like や glob のようなある程度複雑な条件式を利用できます。 別に両手両足を縛られているというほど窮屈な訳では無いのですが、やはりもう少し柔軟なクエリを発行したいなぁ… と思うのが人の常です。


今回私は ContentResolver を使用して、端末が把握している全ての音楽データから、特定の条件にマッチするものを集めたいという意図で利用する事にしました。 何種類かの条件がありましたが、そのほとんどは取り立てて問題もなく結果を取得できました。

ただ、音楽が販売された年度情報で楽曲をまとめ、「2011年は 15曲のデータがあります」的な事をしたいと思ったのですが、先ほどのメソッドではこれが実現できません。

このクエリを、select AlbumColumns.FIRST_YEAR as AudioColumns._ID, count(AudioColumns.ALBUM) as AudioColumns.ALBUM from Audio.Albums.EXTERNAL_CONTENT_URI where AlbumColumns.FIRST_YEAR == ? group by AlbumColumns.FIRST_YEAR order by AlbumColumns.FIRST_YEAR asc のような感じにしたいのです。

要するに、AlbumColumns.FIRST_YEAR の値でグループ化し、その値ごとに AudioColumns.ALBUM が何種類あるかを調べたかったわけです。

ですが、先ほどの例を見て下さい。 メソッドには group by 句を入れる引数がありません。 つまり、このメソッドを使って group by はできません…。

で終わってしまっては何の意味もないので、少し悪あがきをしてみます。

まず、order by に渡す引数を AlbumColumns.FIRST_YEAR asc から AlbumColumns.FIRST_YEAR asc group by AlbumColumns.FIRST_YEAR に変更してみます。

お こ ら れ た w

まぁそうですよね。 普通に考えるとソートをかけてからグループ化はできません。 じゃぁどうしましょうか。 次の候補は where AlbumColumns.FIRST_YEAR == ? を、where AlbumColumns.FIRST_YEAR == ? group by AlbumColumns.FIRST_YEAR に変更してみます。

ま た お こ ら れ た w

実はこのメソッド、where に渡された引数は、全て ( ) の内側に記述してクエリを発行する仕様なのです。 つまり、先ほどのアプローチでは、where (AlbumColumns.FIRST_YEAR == ? group by AlbumColumns.FIRST_YEAR) という不正なクエリを生成しているのでした(T_T 。

じゃぁどうすればいいんだよ! と切れかけた時、至極もっともな解決方法が浮かびました。 そう、クエリ内部で勝手に ) で閉じちゃって、用が済んだら後で閉められる事を考慮してもう一回 ( で開けてやれば良いんじゃね?

つまり、

where (AlbumColumns.FIRST_YEAR == ? )group by (AlbumColumns.FIRST_YEAR)

とするわけです。

で き た w w w

こうやって日々制限の隙間をかいくぐる微妙なコードが生産されているわけです。 あり物の制限をうまくすり抜けられると気分が良いですね。 プラットフォーム SDK の変更で将来的に誤動作を起こさない事を祈りましょう。