作成日

16 February 2009

はじめに

連載第4回目は、ADB(Adobe Developer Box)の裏方として活躍しているデータベース「SQLite」についてどのように活用しているかをご紹介したいと思います。

ADBでは、RSSリーダーやおすすめ情報などの受信したデータは全てSQLiteに格納するようにしています。一旦、SQLiteに格納してしまえばオフライン状態でもデータを表示することができ、使い勝手を向上させることができます。

SQLiteは、MySQLやPostgreSQLなどのデータベースに比べると扱えるデータ型などが限られているので複雑なものを格納するには適していませんが、クライアントの一時的なデータ保管場所としては十分使えるデータベースです。

Adobe AIRでは、このSQLiteを利用するためのAPIが用意されているので、データベースへのINSERTやUPDATE、SELECTなどの操作が行えます。

しかしAPIでは、昔ながらの手順でデータベースへのコネクションを取得し、SQLStatementで生成したクエリを発行して結果を取得するという流れになります。これでは、画面側のロジックでSQL文の生成やコネクションなどデータベースのことを意識しなければいけなくなってしまいます。

そこで今回ADBを開発する中でSQLiteをもっと手軽に効率よく操作するため、AIR向けのO/Rマッピングライブラリ「AirRecord(エアーレコード)」を作りました。

AirRecordは、マーチン・ファウラーのデザイン・パターンの1つ「ActiveRecord」にもとづき設計しています。

ActiveRecordのパターンは「アプリケーション側のモデル構造とDB側のテーブル構造を1:1で対応させる」といったシンプルな考え方で必要なクラス数も比較的少なくおさえることができ、ソースコード量も抑えることができます。このため、他のプログラミング言語でも様々な形式で実装されており、Ruby on RailsやCakePHPなどのフレームワークの中でも機能的に組み込まれています。

AirRecordを使うことで、モデル側でデータベースの操作を集約できるので画面とロジック、モデル(データベース)をうまく分離して開発することができます。それでは、AirRecordの仕組みと使い方を説明していきましょう。

AirRecordの仕組みと特徴

AirRecordは、次のような特徴があります。

  • SQL文を書くことなく非常に短い記述でレコードの抽出や書き換えが行えます。
  • テーブル間の関連を表現でき、1回の検索で紐付くデータを抽出することができます。

例えば、今回のADBの場合、RSSのフィードに関連したエントリー(記事)を取得したいときに、「feed.find(id)」と書くだけで全ての情報が連想配列として返すことができます。

このことにより、画面を担当している開発者は、あまりデータベース周りを気にすることなくデータを操作できるので、スムーズに開発を進めることができます。(今回のADBでは、実際にできたと思います)

AirRecordでは次のような仕組みでデータベースを操作することができるようになっています。

AirRecordのインストール

AirRecordは、オープンソースとしてGoogle Codeにて公開しています。

※AirRecordのライセンスは、MIT Licenseです。

AirRecordプロジェクトのページ

インストールは、ダウンロードしたアーカイブ(最新は、airrecord-1.1.zip)の中のairrecord.swcをAIRプロジェクトのlibsにコピーするだけです。

ここからは、AirRecordプロジェクトページからダウンロードできるサンプルソースをベースに説明していきます。

データベースの接続

アプリケーションの初期処理の中でデータベースへの接続を行います。

AirRecordでは、シングルトンのARDatabaseクラスがコネクションを管理しています。まずは、ARDatabaseのインスタンスに対してaddメソッドでデータベースの接続情報を追加し、connectメソッドで接続します。

var db:ARDatabase = ARDatabase.instance; db.add(userID, password, dbName, ddl, name, path); db.connect();

addメソッドでは、下記のパラメータを設定します。

  • userID: DBのユーザーID
  • password: DBのパスワード
  • dbName: DB名
  • ddl: 流し込むSQL文の配列
  • name: コネクション識別名(デフォルトは、"")
  • path: DBファイルのパス(デフォルトは、app-storage:/)

AirRecordでは、dbNameで指定した名前でSQLiteのファイルを生成します。例えば、dbNameにadbを指定するとadb.dbというDBファイルが作られます。(既にファイルが存在している場合は、DBファイルは生成されません)

DBファイルの生成場所は、pathパラメータでも指定できますが、デフォルトで「app-storage:/」になります。(AIRアプリのストレージ格納場所)

ちなみにAIRアプリのストレージ格納場所は、下記のようになります。
|Windows XP|C:\Documents and Settings\(ユーザー名)\Application Data\(AIRアプリケーションID)\Local Store|
|Windows Vista|C:\Users\(ユーザー名)\AppData\Roaming\(AIRアプリケーションID)\Local Store|
|Mac|/Users/(ユーザー名)/Library/Preferences/(AIRアプリケーションID)/Local Store|

また、接続時にはddlパラメータで指定したSQL文の配列を順番に流しこむようになっています。
このddlには、主にCREATE TABLEのSQLを書いておき、テーブルを生成できるようにしておきます。

var ddl:Array = [ "CREATE TABLE IF NOT EXISTS books (" + " id INTEGER PRIMARY KEY," + " name TEXT" + ")", "CREATE TABLE IF NOT EXISTS notes (" + " id INTEGER PRIMARY KEY," + " book_id INTEGER," + " title TEXT," + " body TEXT," + " created REAL," + " modified REAL" + ")", "CREATE TABLE IF NOT EXISTS tags (" + " id INTEGER PRIMARY KEY," + " name TEXT" + ")", "CREATE TABLE IF NOT EXISTS notes_tags (" + " note_id INTEGER," + " tag_id INTEGER" + ")" ]

SQLでは、IF NOT EXISTSを指定することで既にテーブルが存在する場合は実行しないようにできます。
基本的にテーブルの主キーのフィールド名はidにし、INTEGER PRIMARY KEYを指定しておきます。こうすることでidに通番を自動的にふることができます(auto-incrementなフィールドになる)。

それと、これはテーブル設計時のルールなのですが、テーブル名は複数形に統一するようにしています。bookならbooksとし、noteならnotesとします。これはテーブルは、複数レコードが集まったものなので名称は複数形にするべきという考えだからです。
テストなどでデータベース接続時に必ずDBファイルを初期化したい場合は、connectメソッドを次のように記述します。

db.connect("", true);

こうすることで、起動するたびに一旦DBファイルを削除してから新しいDBファイルを生成することができます。

DBファイルの中を確認する方法として筆者の場合、PupSQLiteというWindowsのツールを利用しています。PupSQLiteを使えば簡単にSQLiteのデータをGUIで確認できるので便利です。

モデルクラスの作成

テーブルの数と同じ分だけモデルクラスを作成します。

モデルクラスは、ARModelを継承し、コンストラクタで__tableにテーブル名を指定します。

例えば、booksテーブルに対するモデルクラスは下記のようになります。

public class Book extends ARModel { public function Book() { super(); this.__table = "books"; } }

驚くことに、たったこれだけでモデルクラスとして機能します。

レコードの追加

レコードを追加する場合は、モデルクラスのinsertメソッドを使用します。
予めモデルクラスのインスタンスを生成しておきます。

var book:Book = new Book(); var result:SQLResult = book.insert({name: "My Notebook"}); var bookID:Number = result.lastInsertRowID;

insertメソッドには、フィールド名と値の連想配列(配列オブジェクトとも言う)を引数で渡します。

レコードの追加の場合は、主キーであるidは指定しないでおきます。こうすることでidにインクリメントされた番号が自動的に割り当てられます。
インクリメントされた番号を取得するには、insertの戻り値のSQLResultの中のlastInsertRowIDを使います。

AirRecordでは、連想配列で指定するのが好きではない人のために、別のinsertの方法があります。
予めモデルクラスに、フィールドに対応したメンバー変数を作っておきます。

public class Book extends ARModel { public var id:Number; public var name:String; public function Book() { super(); this.__table = "books"; } }

insertする前にモデルのインスタンスに対して値を設定しておきます。(idの場合は、Number型なのでNaNを指定する)
そして、引数を指定せずにinsertメソッドを実行します。

book.id = NaN; book.name = "My Notebook"; var result:SQLResult = book.insert();

どちらでも同じことができますが、モデルクラスにメンバー変数を作っておくほうが堅実なやり方です。

レコードの検索

レコードを検索する場合は、モデルクラスのfindメソッドを使用します。

var book:Book = new Book(); var bookData:Array = book.find( {id: 1, name: "My Notebook"}, "name ASC", "10" );

findメソッドの1つ目の引数に検索条件を指定します。
検索条件は、文字列で指定する方法と連想配列で指定する方法があります。

連想配列の場合は、フィールド名とマッチさせる値の組み合わせを指定するとAND検索することができます。

上記の例では、下記のようなSQLが発行されることになります。

SELECT * FROM books WHERE id = 1 AND name = 'My Notebook' ORDER BY name ASC LIMIT 10'

文字列の場合は、WHERE句に指定する条件文を下記のように直接書きます。AND検索以外や、一致検索(A = B)でない場合は文字列で指定します。

var bookData:Array = book.find( "id = 1 AND name = 'My Notebook'", "name ASC", "10" );

findメソッドの2つ目の引数はソート条件(ORDER句)になり、3つ目は取得件数(LIMIT句)が指定できます。

引数を指定しない場合は、全件を取得することができます。

検索結果は、次のようなArrayオブジェクトで返ってきます。

bookData = [0] {id: 1, name: 'aaa'}, [1] {id: 2, name: 'bbb'}, [2] {id: 3, name: 'ccc'}

1件だけを取得したい場合は、findOneメソッドを使う方が便利です。

var bookData:Object = book.findOne({id: 1});

findOneメソッドでは、連想配列として結果が返ってきます。

bookData = { id: 1, name: 'aaa' }

複雑なSQLで検索したい場合は、queryメソッドを使用してください。
queryメソッドでは、SQL文をそのまま発行してSQLResultで結果を返します。

var result:SQLResult = book.query( "SELECT * FROM books WHERE name LIKE 'My%'");

レコードの更新

レコードを更新する場合は、モデルクラスのupdateメソッドを使用します。

var book:Book = new Book(); book.update( {id: 1}, {name: "Sample Book"} );

1つ目の引数に更新レコードの条件を連想配列もしくは文字列で指定します。(条件指定の方法は、findと同じです)
2つ目の引数に更新対象のフィールドと値のセットを連想配列で指定します。

insertと同じようにモデルクラスに値をセットして更新することもできます。その場合は2つ目の引数を省略します。

book.id = 1; book.name = "Sample Book"; book.update( {id: 1} );

但し、この場合は全てのフィールドをモデルにセットしておかなければいけないので注意してください。特定のフィールドのみを更新するには連想配列で指定してください。

レコードの削除

レコードを削除する場合は、モデルクラスのdelメソッドを使用します。

book.del({id: 1});

引数に削除レコードの条件を連想配列もしくは文字列で指定します。(条件指定の方法は、findと同じです)

アソシエーション

AirRecordでは、アソシエーション(関連付け)を記述することで関連したテーブルの情報を自動的に取得できます。

例えば、booksテーブルとnotesテーブルが1:nの関係で関連しているのであれば、下記のようにモデルクラスのコンストラクタでアソシエーションを定義しておきます。

public class Book extends ARModel { public function Book() { super(); this.__table = "books"; this.__recursive = 2; this.__hasMany = { "notes": new ARAssociation("Note", "book_id") }; } }

関連が1:nの場合は__hasManyに連想配列でキーとARAssociationオブジェクトをセットします。(キーを変えて複数指定することができます)

ARAssociationオブジェクトをnewする際に、1つ目の引数で対象のモデルクラス名、2つ目の引数に外部キーとなっているフィールド名を指定します。

これで、Bookのモデルクラスからレコードの検索を行うと、検索結果の連想配列にnotesというキーでNoteモデルクラスに対応したnotesテーブルの関連データが自動的にセットされるようになります。

この状態で、booksから1件だけデータを取得すると下記のような結果が得られます。

bookData = { id: 1, name: 'aaa', notes = [0] {id: 1, book_id: 1, title: 'note1'}, [1] {id: 2, book_id: 1, title: 'note2'}, [2] {id: 3, book_id: 1, title: 'note3'}, }

同様に関連が1:1の場合、親となるモデルでは__hasOneを指定し、子のモデル側では__belongsToを指定します。
n:nの場合は__hasAndBelongsToManyを指定します。(この辺はサンプルソースを見てもらうほうが理解が早いと思います)

アソシエーションの考え方は少し難しい部分もありますが、慣れると1回の検索で関連データが一気に取得できるので非常に便利です。

何階層まで関連データを取得するかは、__recursiveで指定できます(デフォルトは1)。
__recursiveが2の場合は2階層先までの関連データを取得してきます。-1にすると最下層まで取得するようにもできます。

他にも関連データを取得する際に件数を制限したり検索条件を加えたりしたい場合は、ARAssociationオブジェクトをnewする時の引数を変えることで実現できます。

アソシエーションには2つだけ注意点があります。

1つめが、関連データを自動的に検索するため、アソシエーションを定義しすぎるとボトルネックになるということです。1回の検索でどこまでデータを取得したいか利用シーンを考えながら最低限の設定にしておくほうがよいでしょう。

2つめが、アソシエーション定義したモデルクラスがコンパイル対象にならないということです。FlexやAIRアプリケーションをコンパイル(ビルド)すると、内部で使用されていないクラスは除外されてしまいます。
アソシエーションで動的にクラス名を指定しているだけの場合は、下記のようなモデルクラスを参照するだけのModelClassesクラスを作っておき、どこかの場所でこのModelClassesを参照するようにしておくと全てのモデルをコンパイル対象にすることができます。

package sample.notepad.model { public class ModelClasses { Book; Note; Tag; NoteTag; } }

複数データベースの対応

AirRecordでは、1つのAIRアプリケーションで複数のデータベースを操作することもできます。

その場合は、まずデータベースの接続時の引数でnameの部分を別の名前で登録しておきます。(下記の例では、db1とdb2にしています)

var db:ARDatabase = ARDatabase.instance; db.add(userID1, password1, dbName1, ddl1, "db1", path); db.add(userID2, password2, dbName2, ddl2, "db2", path); db.connect("db1"); db.connect("db2");

そして、モデルクラスで__dbNameにどのデータベースが対象か指定します。

public class Book extends ARModel { public function Book() { super(); this.__table = "books"; this.__dbName = "db1"; } } public class Note extends ARModel { public function Note() { super(); this.__table = "notes"; this.__dbName = "db2"; } }

これでデータベースファイルを分けることができます。ファイルを分割したほうがパフォーマンスがアップする場合もあるので、そこは用途に応じて試してみてください。

データベースを分割した場合にアソシエーションは大丈夫か?と心配になるかもしれませんが、現時点ではアソシエーション定義されたテーブルは別々に検索しているので問題ありません。

まとめ

第4回では、AIRアプリケーションで簡単にSQLiteを扱うための方法として自作ライブラリ「AirRecord」をご紹介しました。
AirRecordは、他言語のActiveRecordの実装と比べて機能的には十分ではありませんが、できるだけ軽くシンプルにすることを目標にしているので、たった7つのクラスだけで実装しています。オープンソースとして公開していますので、見てもらえると何かヒントになるかもしれません。もちろんバグがあったら修正してもらえるとうれしいです。

SQLiteをうまく使いこなすことでRIA、クライアントアプリケーションの新たなる可能性が出てくると思います。
Adobe Developer Boxは、まだまだベータ版ですが、パワーアップさせていきますのでご期待ください!!