Piece_ORMでSQLite3を使ってみる

Piece_ORMPiece Frameworkのプロダクトのひとつであり、単体でも使えるO/Rマッパです。非常に使いやすく、Piece_Unityを使わない人にもおすすめです。使い方は公式サイトのほか、くまっち先生のブログでも紹介されています。(http://hatotech.org/kumatch/archives/2008/03/18/piece_orm_php.html)
現在のところPiece_ORMは内部でPEAR::MDB2を使っており、PostgreSQLMySQLSQL Serverをサポートしています。今回は個人的にPiece_ORMでSQLite3のデータベースを操作することに挑戦してみました。無理矢理ながら、どうにか実現できたので、その過程と結果を記しておきます。

MDB2のSQLite3ドライバを書く

先にも述べたように、Piece_ORMでは内部でMDB2を使っているのですが、MDB2はSQLite3をサポートしていません。そこで、MDB2_Driver_sqliteをベースにMDB2_Driver_sqlite3を作成することにしました。
SQLite3のデータベースを操作するのにPDO_SQLiteを使いたかったのですが、MDB2_Driver_sqliteのコードを見た限りでは、PDOのAPIMDB2APIで上手くラップするのは面倒くさそうな予感がしたので、代わりにPECL::sqlite3 (以下、小文字の“sqlite3”はPECL::sqlite3を指すものとします) を使ってみることにしました。
ところがこのsqlite3、α版だけあって(?)、エラー時にE_WARNINGを発生させるだけで、エラーコードやエラーメッセージを取得するメソッドがありません。しかしあきらめるのはまだ早い。ここでPECLerの本領発揮です。俺曰く、「メソッドが無いなら、実装すればいいじゃない」。

sqlite3を拡張するエクステンションを書く

というわけで、MDB2_Driver_sqlite3の制作はひとまず置いといて、sqlite3のSQLite3クラスにエラーコードを表す定数とerrorCode()メソッド、errorMessage()メソッドを追加することにします。
sqlite3自体にパッチを当てても良いのですが、今回は組み込みクラスへのMix-inをやってみました。“sqlite3”とは別の“sqlite3e”エクステンションを作成し、その初期化関数でMix-inします。

インストールはいつもの手順とほぼ同じですが、config.m4を手抜きしているのでSQLite3のインストール先を明示してやったり、php_sqlite3.hを手動でコピーする必要があります。

tar xfz php_sqlite3e-0.0.1.tgz
cd php_sqlite3e-0.0.1
mkdir -p ext/sqlite3
cp /path/to/pecl-sqlite3-0.4/php_sqlite3.h ext/sqlite3
CPPFLAGS=-I/path/to/sqlite3-install-dir/include ./configure --enable-sqlite3e
make
sudo make install

sqlite3エクステンションがロードされている状態でsqlite3eエクステンションをロードすると、SQLITE3_OK等の定数が追加され、SQLite3クラスにerrorCode()メソッドとerrorMessage()メソッドが追加されます。

MDB2のSQLite3ドライバをでっち上げる

MDB2_Driver_sqlite3の制作に戻ります。まずMDB2_Driver_sqlitesqlite.phpをsqlite3.phpにリネームし、クラス名接尾辞のsqliteをsqlite3に置換した後、sqlite関数をコールしている箇所をsqlite3のメソッドに変更します。また、sqlite3は持続的接続や結果セットのバッファリングには対応していないようなので、connect()メソッドでそれらのオプションが有効な場合はエラーを返すようにします。
ドライバ本体以外のMDB2モジュール、Datatype、Function、Manager、Native、Reverseも修正する必要があるのかもしれませんが、とりあえずそのままにしておいて、動かなかったら直す方針で。

Piece_ORMをMDB2_Driver_sqlite3に対応させる

Piece_ORMでMDB2_Driver_sqlite3を使うには、MDB2のデータ型とSQLite3ネイティブのデータ型を対応させるためのクラスを作成する必要があります。例えば、Piece_ORM_MDB2_NativeTypeMapper_Pgsqlではtimestamptz型や幾何データ型をマッピングしていますが、Piece_ORM_MDB2_NativeTypeMapper_Sqlite3では何もしません。ただクラスを用意しただけです。

下準備

実際に使ってみる前に、まず最低限必要なファイルとディレクトリを用意します。

mkdir cache config mappers
touch config/piece-orm-config.yaml mappers/Test.yaml
sqlite3 test.sq3 'CREATE TABLE test ("id" INTEGER PRIMARY KEY, "content" TEXT)'

テーブルを作成する際には、MDB2_Driver_Reverse_sqlite3 (がベースにしたMDB2_Driver_Reverse_sqlite) の仕様上の制限により、主キーのカラム名をクォートするか、主キー制約をカラム定義とは別に書かないと主キーを取得できないので注意してください。
つまり、

CREATE TABLE test (
  id INTEGER,
  content TEXT,
  PRIMARY KEY(id)
);

や、

CREATE TABLE test (
  "id" INTEGER PRIMARY KEY,
  "content" TEXT
);

はOKですが、

CREATE TABLE test (
  id INTEGER PRIMARY KEY,
  content TEXT
);

はNGとなります。

追記:主キーをオートインクリメントカラムと認識させるには PRIMARY KEY(id) (1番目の方法) ではだめで、"id" INTEGER PRIMARY KEY (2番目の方法) としなければいけませんでした。

piece-orm-config.yamlは以下のように記述します。MDB2_Driver_sqlite3は結果セットのバッファリングをサポートしないので、result_bufferingを明示的に無効にしなければいけません。

- name: database1
  dsn: sqlite3:///./test.sq3
  options:
    result_buffering: no

使ってみる

単純なINSERT/SELECT/UPDATE/DELETEをしてみましょう。

<?php
extension_loaded('sqlite3') || dl('sqlite3.so');
extension_loaded('sqlite3e') || dl('sqlite3e.so');
require_once 'Piece/ORM.php';

Piece_ORM::configure('config', 'cache', 'mappers');

$mapper = Piece_ORM::getMapper('Test');

$foo = new stdClass();
$foo->content = 'foo';
$mapper->insert($foo);

$bar = new stdClass();
$bar->content = 'bar';
$mapper->insert($bar);

$baz = new stdClass();
$baz->content = 'baz';
$mapper->insert($baz);

print_r($mapper->findById($foo));
print_r($mapper->findAll());

$bar->content = 'qux';
$mapper->update($bar);

$mapper->delete($baz);

print_r($mapper->findAll());

結果:

stdClass Object
(
    [id] => 1
    [content] => foo
)
Array
(
    [0] => stdClass Object
        (
            [id] => 1
            [content] => foo
        )

    [1] => stdClass Object
        (
            [id] => 2
            [content] => bar
        )

    [2] => stdClass Object
        (
            [id] => 3
            [content] => baz
        )

)
Array
(
    [0] => stdClass Object
        (
            [id] => 1
            [content] => foo
        )

    [1] => stdClass Object
        (
            [id] => 2
            [content] => qux
        )

)

大丈夫なようですね。

今後の予定とか

今回作ったものは「とりあえず動く」程度のもので、ユニットテスト (Piece_ORM_Mapper_Sqlite3TestCase) を制作、実行して品質を向上させる必要があります。
sqlite3の拡張に関しては、エラー周りの定数とメソッドの実装をリクエストするつもりです。実装は済んでいるので、あとはパッチを送るだけの簡単なお仕事です。