前提条件
この記事は、下記の事項を既に理解していることを前提としています。
ROOMの基本的な利用方法
SQLの基本構文
Kotlinの基本(Collectionなど)
これらの基本事項については、記事内でカバーしません。そのため、アプリ開発初心者の方にとってはやや説明不足のように感じられるかもしれませんが、ご了承下さい。
また、冒頭でも述べたように今回は『シンプルな絞り込み検索』を例にします。下表のように、【ID(主キー)】、【名前】、【チーム名】を持つテーブルをイメージして下さい。
テーブル名:sample_table
id
name
team
1
Tom
A
2
Marie
A
3
Dan
B
4
Keiko
C
ここで、チーム名で絞り込み検索を行う例を考えてみましょう。以下のパターンが考えられます。
A、B、Cの全てのチームを含む
Aチームのみ
Bチームのみ
Cチームのみ
A、Bチーム(Cチームを除く)
A、Cチーム(Bチームを除く)
B、Cチーム(Aチームを除く)
たとえば『3つのチームのうち、AチームもしくはBチームであること』を検索の条件とする場合のSQL文は、次のように記述することができます。
SELECT * FROM sample_table WHERE team = 'A' OR team = 'B';
▼
このようにチーム名(A〜C)を絞り込みの条件としてユーザー側で指定できるようにしたい場合、WHERE以下の条件文を動的に生成してクエリに組み込む必要があります。
この絞り込み検索の例を使って、一見問題なさそうに見えるのに動的なクエリの生成が機能しないアプローチと、期待通り動作するアプローチを確認してみたいと思います。
うまくいかないアプローチ
成功例を確認する前に、まずは陥りやすい失敗例を確認しておきましょう。
ROOMでは、コロン【 : 】を用いてクエリに受け取った引数を渡すことが可能です。具体的な使用例は下の通りです。
@Query ("SELECT * FROM sample_table WHERE id = :id" )
fun getItem (id: Int ): List <MyEntity >
▼
これを利用すれば、動的にクエリを生成して実行することも難しくないように思えるかもしれません。たとえば、チーム名を更新可能な List
や Set
で定義しておき、それを引数として受け取ってクエリに埋め込まれるようにする方法が考えられます。
たとえば、A、B、Cのチームのうち、AもしくはBチームであること(Cチーム以外)を絞り込みの条件とする場合のコードを次のように記述してみましょう。
val teams = mutableSetOf ('A', 'B', 'C')
teams.remove ('C')
@Query ("SELECT * FROM sample_table WHERE :query" )
fun getItemsByQuery (query: String ): List <SampleEntity >
fun getItemsByMultipleQueries (teams: Set <Char >): List <SampleEntity > {
val query = teams.joinToString (" OR " ) { "team = '$it'" } // team = 'A' OR team = 'B'
return getItemsByQuery(query)
}
▼
最終的にクエリは下のようになりますが、この部分だけ見ると完璧で何の問題もないように思えます。
SELECT * FROM sample_table WHERE team = 'A' OR team = 'B'
▼
しかし、最初に言ったようにこれは失敗してしまう例です。仮にこの状態でアプリをビルドしてクエリを実行しても、テーブルから行は一行も取得することができません。(アプリのビルド自体は成功します)
これは、動的に生成されたクエリが全て単なる文字列として解釈されてしまい、 =
や OR
といった特別な意味をもつ語句が機能しなくなるためだと思われます。
では、どうすればいいのでしょうか?次のステップで期待通りにちゃんと機能するケースを確認していきましょう。
期待通りの結果が得られるアプローチ
絞り込み検索など、ユーザーの指定に応じて動的にクエリを生成したい場合は @RawQuery
アノテーションを利用します。これにより、動的に生成したSQLクエリを実行できるようになります。
@RawQuery
suspend fun getItemsByQuery (query: SupportSQLiteQuery ): List <SampleEntity >
▼
@RawQuery
アノテーションで定義されたメソッドは、SupportSQLiteQuery
オブジェクトをパラメタとして受け取る必要があります。SupportSQLiteQuery
は、動的なSQLクエリを安全に実行するためのクラスです。
以上を踏まえて上記のコードの続きを記述すると、下のようになります。
@RawQuery
suspend fun getItemsByQuery (query: SupportSQLiteQuery ): List <SampleEntity >
suspend fun getItemsByMultipleQueries (teams: Set <Char >): List <SampleEntity > {
val placeholders = teams.joinToString (" OR " ) { "team = ?" }
val args = teams.map { "$it" }.toTypedArray()
val query = SimpleSQLiteQuery ("SELECT * FROM sample_table WHERE $placeholders" , args)
return getItemsByQuery(query)
}
▼
teams
パラメタとして受け取った Set<Char>
から動的にクエリを生成するためのプレースホルダー(placeholders
として定義)及び、Array(args
として定義)を宣言し、それぞれ、SimpleSQLiteQuery
に渡すことで動的なクエリが生成されます。
最終的に、getItemsByQuery
メソッドにクエリ(SupportSQLiteQuery
)を渡して実行することで、動的なクエリが完全に実行され、結果(List
)を得ることができます。
下の画像は、実際に絞り込み検索が正常に機能していることを表すものです。
RawQuery
はAndorid SQLite APIにおける概念であり、SQLiteやSQLについて調べても情報が全く出てこない点には注意が必要です。
RawQuery(Android SQLiteのクエリ)についての基本は、下の公式ドキュメントからご確認いただけます。
・SQLite 入門(Android SQLite のクエリ)
https://developer.android.com/courses/extras/sql-primer?hl=ja#queries-for-android-sqlite
最初はシンプルな構造のテーブルを使って、色々なクエリを試してみると理解しやすいかと思います。