データベースの作成
AIRランタイムにはSQLiteというオープンソースのデータベース機能が組み込まれており、標準的なSQLを使ってローカルデータを管理できます。ファイルAPIでもローカルファイルの読み書きは可能ですが、例えばアドレス帳のように大量のデータを保存してランダムに検索したい場合などはデータベースの方が適しています。
データベースは単一のローカルファイルを使用します。このファイルには可搬性があり、好きな場所に保存できます。実際に作成してみましょう。
上記のコードを実行すると、デスクトップにsample.dbというファイルが作成されます。これがデータベースファイルです。ここで行っているのはデータベースをオープンする処理です。データベースを使用するときは、毎回このようにSQLConnectionオブジェクトを作成し、openAsync()メソッドでオープンする必要があります。ファイルの指定にはFileオブジェクトを使います。指定のファイルが存在しないときは自動的に作成されます。ファイル名は拡張子も含めて任意で構いません。ファイルを指定しない場合はメモリ内にデータベースが作成されます。データベースを使い終わったらclose()メソッドでクローズします。
なお、openAsync()の代わりにopen()を使う方法もあります。openAsync()の場合はその後の処理が非同期となり、open()の場合は同期処理となります。同期処理はコードの流れを追いやすい等のメリットがありますが、扱うデータ量によっては他の処理を止めてしまうため、基本的には非同期処理を使ったほうが無難でしょう。
SQLの実行
データベースに対する操作はSQLで行います。SQLを実行するにはSQLStatementオブジェクトを使います。次のコードはテーブルを作成するサンプルです。データベースsample.dbをオープンして、2つのカラムfoodとpriceを持つテーブルmenuを作成しています。
上のサンプルではcreateTable()メソッド内でSQLを実行しています。SQLStatementオブジェクトのsqlConnectionプロパティには実行対象のデータベースを指定します。値はSQLConnectionオブジェクトです。その上でtextプロパティにSQL文を設定し、execute()メソッドを呼び出すことでSQLが実行されます。正しく実行された場合はSQLEvent.RESULTイベント、エラーが発生した場合はSQLErrorEvent.ERRORイベントが送出されます。
レコードの操作結果
レコードの挿入や抽出等の操作を行った場合も、処理が正しく完了したときはSQLEvent.RESULTイベントを受け取ります。このタイミングでSQLStatementオブジェクトのgetResult()メソッドを呼び出すと処理結果を調べられます。次のコードはレコードを1件追加する例です。
コードから分かるように、getResult()メソッドの戻り値はSQLResultオブジェクトです。SQLResultオブジェクトにはいくつかのプロパティがあります。lastInsertRowIDプロパティは、最後に追加したレコードのIDを返します。テーブルに整数値型のプライマリキーが1つ定義されていればその値が使われますが、それ以外の場合はデータベースが自動で生成するユニークIDが使われます。
次のコードはpriceカラムが100以下のレコードを抽出する例です。
今度はSQLResultオブジェクトのdataプロパティを取得しています。これは抽出したレコードの配列です。各要素はカラム名をプロパティとするObjectオブジェクトです。レコードが1件も一致しなかった場合、dataプロパティはnullになります。
抽出結果を分割して受け取る
抽出条件に一致するレコードが多数ある場合、それらを一度に受け取ると処理の負荷が高くなります。そこで、必要な件数を部分的に受け取る方法が用意されています。前述のコードを変更して、10件ずつ取得する例を示します。
上のコードのようにexecute()メソッドのパラメータに件数を指定することで、最初に取得するレコード数を制限できます。その続きを取得するにはnext()メソッドを使います。next()もexecute()と同様にレコード数の指定ができます。next()の直前にSQLResultオブジェクトのcompleteプロパティを調べていますが、この値がtrueの場合にすべての該当レコードを取得し終わったことになります。
SQL文でパラメータを使う
データベース操作では何度も同じSQL文を使うことがあります。SQL文は実行時にコンパイルが必要なため、毎回SQLStatementオブジェクトを作るより、実行済みのオブジェクトを使い回した方が効率的です。ただしtextプロパティを変更してしまうと意味がありません。構文が同じで値だけが変わるものは、値をリテラルで指定せずパラメータにすることで再利用できます。
また、処理のオーバーヘッドを減らすだけでなく、SQLインジェクション攻撃を防ぐ目的でもパラメータを使います。ユーザーからの入力値を扱う場面では、値を直接SQL文に連結すると危険です。構文をあらかじめ確定できるパラメータ方式で処理してください。下記はパラメータを使った例です。
SQL文の中にはプレースホルダとして@を付けたパラメータ名を記述しておきます。@の代わりに:を使うこともできます。その上で、SQLStatementオブジェクトのparametersプロパティを使って各パラメータに値を設定していきます。
また、パラメータ名を指定せずにインデックスで指定する方法もあります。その場合はプレースホルダとして?を記述します。?の登場順にインデックスが割り当てられるので、parametersプロパティでのパラメータ指定にインデックスを使います。
トランザクション
トランザクションを開始するにはSQLConnectionオブジェクトのbegin()メソッドを使います。トランザクション処理が正しく完了したらcommit()メソッドで変更内容を確定し、途中で失敗したらrollback()メソッドで変更内容を破棄します。次のコードはトランザクションを使ってレコードを追加する例です。
SQLiteではINSERTやUPDATE、DELETE等で更新処理を行う際に、1件ずつ暗黙的なトランザクションが使われます。毎回ディスクへの書き込みが発生するため、データ量に応じた時間がかかります。これに対して上記メソッドで明示的にトランザクションを使用すれば、開始から終了までの処理がメモリ上で行われるため、多数のデータを更新する場合にパフォーマンスが向上します。
なお、ここでは前のINSERTが完了してから次を実行していますが、非同期処理の場合でもイベントを待たずに連続してSQLを実行することは可能です。その場合はSQLConnectionオブジェクトのキューに追加され、順番に実行されます。ただし前の実行結果に依存する処理はイベントを待ってから実行する必要があります。
SQLite用ツールの利用
テスト用にデータベースを用意したりちょっとした内容確認をしたいときなどは、SQLite用のデータベース管理ツールを併用すると便利です。SQLite Database BrowserやFireFoxアドオンのSQLite Managerなどがあり、データベースの作成やレコードの追加/削除、SQLの実行といった作業をGUIで行えます。