はちゅにっき

こっちのブログはまったり更新

PDOxSkinny - Intro

ドキュメントまでコピーでほんとごめんなさい。

PDOxSkinny を利用するための最小限の雛形は以下のようになります。
Schema Class は存在だけしていれば、スキーマの定義がなくてもある程度動きます。

<?php
require_once 'Skinny.php';
require_once 'Skinny/Schema.php';

// Model Class
class MyModel extends PDOxSkinny { }

// Schema Class  (Model Class 名 + Schema で定義する必要があります)
class MyModelSchema extends SkinnySchema
{
    function __construct ( )
    {
        $this->install_table(
            ...
        );
    }
}

注意

現在 PDOxSkinny は PostgreSQLMySQLSQLite をサポートしているかもしれません。
Oracle など他の Driver を使いたい場合は、 SkinnyDriver** を作っていただく必要があります。
まだまだ α版なので色々 変わる事があるかもしれません。 バグっています。
また、後方互換を考えずに変更する可能性があります。

Skinny の基本クラスの定義

Skinny を操作する Class を定義します。
例えば Proj というプロジェクトで Skinny を使う場合

<?php
require_once 'Skinny.php';

class ProjModel extends PDOxSkinny
{
    function __construct ( )
    {
        parent::__construct( array(
            'dsn'      => 'pgsql:dbname=foo'
            'username' => 'username',
            'password' => 'password',
        ) );
    }
}

このような Class を用意します。

コンストラクタをオーバライドしたくない場合は、Class を作成するときに、引数で dsn など DB の接続に必要な情報を渡せば OK です。

<?php
require_once 'proj.php';

$obj = new ProjModel( array(
    'dsn'      => 'pgsql:dbname=foo'
    'username' => 'username',
    'password' => 'password',
) );

dsn などを書かずに Class を作成しておき、 DBにクエリを投げる前に

<?php
$obj->connect_info(....); # connect_infoを設定する

もしくは

<?php
$obj->connect(....): # connectメソッドをdsnなどの引数ともに呼び出す

としてもよいです。
また元々 db の handler を別で持っている場合でそれを使い回したい場合は

<?php
$obj2 = new ProjModel($obj);

このようにコンストラクタに渡してやれば、内部で持っている Database Handler を置き換える事ができます。

スキーマクラスの定義

Skinny では他の OR マッパーと同じように各 table に対応する schema の設定を書く必要があります。
例えば user テーブルがある場合

<?php
require_once 'Skinny/Schema.php';

class ProjModelSchema extends SkinnySchema
{
    function __construct ( )
    {
        $this->install_table('user', array (
            'pk'      => 'id',
            'seq'     => 'user_id_seq',   
            'columns' => array(
                'id', 'guid', 'login_id', 'login_pw',
                'name', 'mail', 'created_at', 'updated_at',
            ),
        ) );
    }
}

この例では user テーブルのプライマリキーは id であり、各カラムには id / guid / login_id / login_pw / name / mail / created_at / updated_at がある事を定義しています。
また、PostgreSQL を利用している場合には、プライマリキーのシーケンスがあれば、そのテーブルを指定します。*1
Skinny では他の OR マッパーと異なり、テーブル毎に Class を作る必要はありません。
この例の場合 ProjModelSchema に全てのテーブル情報を記載します。
スキーマの定義は、スキーマクラスのコンストラクタとして定義するか、スキーマクラスに
register_schema( ) メソッドを実装し、そこに記述していきます。
また、register_schema メソッドを実装した Schema クラスをあらかじめ作成しておき、Model クラスのコンストラクタにスキーマオブジェクトを渡すこともできます。
これを利用すると、スキーマクラスのコンストラクタなどでクラスのプロパティなどを設定することができるようになります。

<?php
require_once 'proj.php';
require_once 'proj_schema.php'

$schema_class = new ProjModelSchema( array('operator' => 'hatyuki') );
$schema_class->permittion = 'ro';

$obj = new ProjModel( array(
    'dsn'      => 'pgsql:dbname=foo'
    'username' => 'username',
    'password' => 'password',
    'schema'     => $schema_class,
) );

inflate / deflate の処理について

Skinny にも inflate / deflate の処理を書く事ができます。

<?php
require_once 'Skinny/Schema.php';

class ProjModelSchema extends SkinnySchema
{
    function __construct ( )
    {
        $this->install_table('locales', array(
            'pk'      => 'id',
            'columns' => array(
                'id', 'locale', 'display_name',
                'created_by', 'created_at', 'updated_by', 'updated_at',
            ),
        ) );

        $this->install_inflate_rule('^.+_at$', array(
            'inflate' => array($this, 'inflate_time'),
        ) );

    function inflate_time ($data)
    {
        return 0;
    }
}

例えばこのように書くと、***_at のカラムを取得するとすべて 0 になりますが、現実には DateTime に Inflate / Deflate するなど、建設的に使うべきです。
install_inflate_rule に対象となるカラムのルールを書きます。 ここは正規表現で書きます。
install_inflate_rule は Skinny で扱う全テーブルが対象となります。

trigger について

Skinny にも insert / update / delete などを行った場合に trigger による Hook をかける事ができます。
例えば insert 時に created_at を自動で設定する trigger をかけたい場合は

<?php
require_once 'Skinny/Schema.php';

class ProjModelSchema extends SkinnySchema
{
    function __construct ( )
    {
        $this->install_table('user', array(
            'pk'      => 'id',
            'columns' => array(
                'id', 'guid', 'login_id', 'login_pw',
                'name', 'mail', 'created_at', 'updated_at',
            ),
            'trigger' => array(
                'pre_insert' => array(
                    array($this, 'insert_trigger'),
                    ... // other trigger of pre_insert
                ),
            ),
        );
    }

    function insert_trigger ($skinny, &$args, &$table)
    {
        $args['created_at'] = 'NOW( )';
    }
}

例えばこのように書きます。
現在トリガーを設定できるポイントは

pre_insert / post_insert / pre_update / post_update / pre_delete / post_delete

があります。
トリガーはテーブル単位で設定する事ができます。
またトリガーは同じ Hook ポイントに対して複数設定する事もできます。同じ Hook ポイントに複数設定した場合は設定した順番に実行されます。
また、トリガーメソッドの引数は参照受け渡しですので注意してください。
"&" を忘れると悲しい思いをすることになるかもしれません。

new

PDOxSkinny ではインスタンスを作ってDBを操作します。

<?php
$model = new ProjModel( array(
    'dsn'      => 'pgsql:dbname=test',
    'username' => 'username',
    'password' => 'password'
    'connect_options' => array(
        'AutoCommit'    => false,
        'on_connect_do' => array(
            'SET search_path TO my_project',
        ),
    ) )
) );
...

connection_info / connect /reconnect

connection_info

connect_infoメソッドではDB接続情報を設定します

<?php
$model->connection_info( array(
    'dsn'      => 'pgsql:dbname=test',
    'username' => 'username',
    'password' => 'password'
    'connect_options' => array(
        'AutoCommit'    => false,
        'on_connect_do' => array(
            'SET search_path TO my_project',
        ),
    ) )
) );

connection_info メソッドを呼び出した時点では DB の接続は確立されません。

connect

明示的にDB接続を行いたい場合は connect メソッドを使用します。

<?php
$model->connect( array(
    'dsn'      => 'pgsql:dbname=test',
    'username' => 'username',
    'password' => 'password'
    'connect_options' => array(
        'AutoCommit'    => false,
        'on_connect_do' => array(
            'SET search_path TO my_project',
        ),
    ),
) );
reconnect

一度DBに接続された状態で他のDBに接続しなおしたい場合は reconnect メソッドを使用します。

<?php
$model->reconnect( array(
    'dsn'      => 'pgsql:dbname=test',
    'username' => 'username',
    'password' => 'password'
    'connect_options' => array(
        'AutoCommit'    => false,
        'on_connect_do' => array(
            'SET search_path TO my_project',
        ),
    ),
) );

reconnect メソッドを呼び出すと呼び出す前まで保持していた Database Handler は破棄されます。

dbh

dbh メソッドを呼び出すとその時点での Database Handler が取得できます。

<?php
$dbh = $model->dbh( );

query

query メソッドは PDO::query( ) のショートカットになっています。

<?php
$model->query("
    CREATE TABLE foo (
        id   INT,
        name TEXT
    )
");

insert / create

user テーブルにレコードを insert するには以下のようにします。

<?php
$row = $model>insert('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ gmail.com',
) );

insert メソッドの返り値は Skinny の Row クラスになっていますので

<?php
print $row->name( ); # hatyuki
print $row->mail( ); # hatyuki _at_ gmail.com

このようにカラム名をメソッドとしてデータにアクセスできます。
また、create メソッドは insert メソッドのエイリアスになっているのでどちらでも OK です。

<?php
$row = $model->create('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ gmail.com',
) );

update

user テーブルのレコードを update するには以下のようにします。

<?php
$model->update('user', array('name' => 'nekokak'), array('id' => 1));

一つ目の hash が更新する情報で、二つ目の hash が更新対象とするレコードの条件です。
また、Row クラスから直接 update をかけることもできます。

<?php
$row = $model->insert('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ gmail.com',
) );

$row->update( array('name' => 'nekokak') );

delete

user テーブルのレコードを delete するには以下のようにします。

<?php
$model->delete('user', array(id => 1));

hash で delete 対象とするレコードの条件を指定できます。
また delete メソッドも update メソッドと同じく Row クラスから直接 delete をかけることもできます。

<?php
$row = $model->insert('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ gmail.com',
) );

$row->delete( );

bulk_insert

user テーブルに一気に複数行 insert をかけたい場合は以下のようにします。

<?php
$model->bulk_insert('user', array(
    array(
        array(
            'name' => 'hatyuki',
            'mail' => 'hatyuki _at_ gmail.com',
        ),
        array(
            'name' => 'nekokak',
            'mail' => 'nekokak _at_ example.com',
        ),
    ),
);

bulk_insert では現状 insert のトリガーは利用できませんのでご注意ください。

find_or_create / find_or_insert

user テーブルに指定した条件のレコードが存在すればその行を select し、レコードが存在しなければ insert を行うことが出来ます。

<?php
$row = $model->find_or_create('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ example.com',
) );

また、find_or_insert メソッドは find_or_create メソッドのエイリアスになっているのでどちらでも OK です

<?php
$row = $model->find_or_insert('user', array(
    'name' => 'hatyuki',
    'mail' => 'hatyuki _at_ gmail.com',
) );

single / search /search_by_sql / count

single

user テーブル1行だけ取得したい場合に使用します。

<?php
$row = $model->single('user', array('name' => 'hatyuki'));
search

user テーブルに対して select クエリを発行する場合に search メソッドを使用します。

<?php
$itr = $model->search('user',
    array('name' => 'hatyuki'),
    array( ),
);

二つ目の hash に検索条件を、三つ目の hash に order や limit などのオプションを渡せます。
細かい検索条件の指定の仕方は DBIx::Skinny::Manual::JA::Resultset を参照してください。

search_by_sql

生の select クエリを発行する場合にはこのメソッドを使います。

<?php
$itr = $model->search_by_sql("SELECT * FROM user WHERE id = ?", array(1), 'user');

一つ目の引数に発行したいクエリを、二つ目の引数に発行したいクエリに使用する bind の値を、三つ目の引数はオプションです。指定しなくてもよいです。
また、search_by_sql は PDO::execute( ) を利用しているため named なプレスホルダーを使いつつ、SQLを実行させることもできます。

<?php
$itr = $model->search_by_sql("SELECT * FROM user WHERE id > :id", array(':id' => 1));
count

user テーブルの count をとりたい場合は count メソッドを使用します。

<?php
$count = $model->count('user' , 'id', array('name' => 'nekokak'));

二つ目の引数が count を取る対象となるカラム情報で、三つ目の引数が count を取る条件となります。

resultset

DBIx::Skinny::Manual::JA::Resultsetを参照してください。

トランザクション

Skinnyではトランザクションの仕組みを簡単にサポートしています。
トランザクションを有効にした処理を書きたい場合は以下のようにします。

<?php
$txn = $model->txn_scope( );

$row = $model->single('user', array(id => 1));
$row->set( array('name' => 'hatyuki') );
$row->update( );

$txn->commit( );

Skinny のトランザクションサポートは txn_scope メソッドで取得したオブジェクトが有効な間、トランザクションの面倒をみます。
$txn->commit( ) を実行するまでの間にデータベースに対して複数の更新クエリを実行させます。
$txn->commitが実行されずに$txnオブジェクトが亡くなってしまった場合、それまでの更新はすべて rollback されます。
txn_scopeメソッドを使わずに

<?php
$model->txn_begin( );

$row = $model->single('user', array(id => 1));
$row->set( array('name' => 'nekokak') );
$row->update( );

$model->txn_commit( );
$model->txn_end( );

自前でトランザクションを管理する事も可能です。
当然ですがトランザクション機能をつかうには RDBMSトランザクションの機能をサポートしている必要があります。
MySQLをお使いの場合はInnoDBを使ってください。

メソッドの追加 (Mixin)

PDOxSkinny では Mixin っぽい機構を提供している気がします。
これを利用すれば ProjModel にメソッドを追加した風に振舞うことができるようになります。
例えば

<?php
require_once 'Skinny.php';
require_once 'Skinny/Mixin.php';

class ProjModel extends PDOxSkinny
{
    function __construct ( )
    {
        $this->mixin( array('MixinFoo') );

        parent::__construct( );
    }
}

class MixinFoo extends SkinnyMixin
{
    function register_method ( )
    {
        return array(
            'foo' => array($this, 'foo'),
        );
    }

    function foo ( )
    {
        return 'foo';
    }

    function bar ( )
    {
        // $this->skinny を介することで Skinny にアクセスできます
        return $this->skinny->single('user', array(
            'name' => 'hatyuki'
        ) );
    }
}


このように MixinFoo で定義された register_method の内容に従って ProjModel にメソッドが export されたかのように振舞います。
この例の場合 foo というメソッドが export されるので

<?php
$model->foo( );

とアクセスする事ができます。
また、mixin( ) はいつでも呼び出すことが可能です。
コンストラクタに書かなければならないといった制約はありませんが、export されたメソッドを利用する前に mixin( ) を実行する必要があります。

Rowオブジェクトへのメソッド追加

ProjModelRow{Table}のようなクラスを用意すること、Skinny は Iterator から返される Row オブエクトのベースとなるクラスで ProjModelRow{Table} を使う事ができます。
Table クラスが見つからない場合や、発行したクエリからどのテーブルクラスを使うべきか判断できない場合は SkinnyRow クラスを使用します。
Table クラスを用意することで、そのクラスにメソッドを定義できます。

<?php
require_once 'Skinny/Row.php';

class ProjModelRowUser extends SkinnyRow
{
    function foo ( )
    {
        print "foo\n";
    }
}

foo メソッドを定義しておく事で

<?php
$row->foo( );

と呼び出す事が可能です。
またこの仕組みを利用すれば、リレーションを独自に実装する事も可能です。
例えば、User has_many Blog の場合

<?php
class ProjModelRowUser extends SkinnyRow
{
    function blogs ( )
    {
        return $this->skinny->search('blog', array(
            'user_id' => $this->id( )
        ) );
    }
}

このように書く事ができ

<?php
$user->blogs( );

このようにアクセスさせる事が可能です。

Skinny のデバッグ

環境変数 "SKINNY_PROFILE" もしくは Model クラスのコンストラクタに適切な値を設定することで、Skinny で発行された SQL を知ることができます。
設定できる値は、以下の値の足し算 (クラス定数の場合は論理和) になります。

クラス定数 動作
0 なにもしません (デフォルト)
1 Skinny::TRACE_LOG SkinnyProfiler クラスが記録します
2 Skinny::PRINT_LOG STDOUT に出力します
4 Skinny::WRITE_LOG タイムスタンプつきでファイルに書き出します

たとえば以下のようにして設定します。

<?php
$model = new ProjModel( array(
    'dsn'      => 'pgsql:dbname=foo'
    'username' => 'username',
    'password' => 'password',

    // STDOUT に出力して、ファイルにも書き出す
    'profile'  => 6,

    // クラス定数を使って書く方法
    'profile'  => Skinny::WRITE_LOG | Skinny::PRINT_LOG
) );

または

$ export SKINNY_PROFILE = 6

TRACE_LOG を指定した場合、SkinnyProfiler クラスによって発行された SQL が記録されます。
記録された SQL

<?php
 $skinny->query_log( );

とすることで配列として取得することができます。
WRITE_LOG を指定した場合のファイルの出力先は、環境変数 SKINNY_LOG で指定することができます。
指定がない場合には、カレントディレクトリに "database.log" という名前で出力されます。

エラーハンドリング

Model クラスのコンストラクタに適切な値を設定することで、SQL 実行時にエラーが発生した際に SkinnyException を発生させることができます。

<?php
$model = new ProjModel( array(
    'dsn'      => 'pgsql:dbname=foo'
    'username' => 'username',
    'password' => 'password',

    // SkinnyException を発生させる
    'raise_error' => true,
) );

raise_error を設定しない場合においても、SQL を実行後に Model クラスの is_error( ) を確認することで、エラー発生の有無を確認することができます。

<?php
$model->search_by_sql($query);

if ( $model->is_error( ) ) {
    print "えらー!\n";
    print $model->get_err_msg( );
}

ただし、query( ) メソッドを利用して SQL を実行した場合には、いずれの方法においてもエラーを検出することができません。

*1:PostgreSQL デフォルトのシーケンスである "table 名"_"primary key 名"_seq を利用している場合には、この記述を省略することができます。例の user テーブルであれば、テーブル名が "user" Primary Key が "id" ですので、"user_id_seq" をシーケンスとして利用している場合は省略することができます。