MySQLのデータを配列の様に扱えるPHPのORM「Arrmy」

ORMって覚えるのが大変です。僕だけしょうか。いや、そういう人は多いはず。多分。

ということで学習コストをなるべく抑えられる様に、MySQLのデータを配列感覚で扱えるPHPのORMを作ってみました。PHP5.5以上で動作します。

Arrmyの目的

これを作った目的は、一番は学習コストを抑えること。ORMを使うメリットを感じられない人も、簡単シンプルに導入できることを目的としています。

また、別の目的として、

  • あらゆるデータモデルをマッピング出来る柔軟性
  • 大規模データを扱う事による非効率性の解消

ということを実現可能にし、柔軟且つ軽量なORMを目指しています。

ちなみに「Arrmy」という名前は「Array(配列)」と「MySQL」をくっつけたものです。で、「Arrmy」が「Army(陸軍)」っぽいよね、っていうことでクラス名が「Tank(戦車)」ってことになっています(下記「使い方」参照)。

という経緯があるので、メソッド名等も厨二病的なノリのものが御座いますが、ご了承いただければと思います。。

使い方

ソースコードをGitHubからダウンロードします。

Arrmy

※ zipでダウンロードした場合に「Arrmy-master」というようなブランチ名が付いたフォルダが作成されたら「Arrmy」に変更して下さい。

まずは以下のようにしてDBサーバーへ接続し、使用するテーブルを指定します。

<?php
require_once 'Arrmy' . DIRECTORY_SEPARATOR . 'Tank.php';
use Arrmy\Tank;

// DBサーバーに接続
$tank = new Tank('localhost', 'user', 'password', 'scheme');
// 使用するテーブルを指定
$tank->target('table');

インスタンス作成時にDBサーバーへの接続情報(ホスト、ユーザー名、パスワード、データベース名)を渡します。
コンストラクタでDBサーバーへ接続されデストラクタで切断されるようになっていますので、特に接続・切断を指示する必要はありません。また、ホスト名、ユーザー名、データベース名が同じであれば接続リソースが共有されるようになっていますので、何度インスタンスを作成してもDBサーバーへの接続リソースが消費されるということはありません。

<?php
// 新規接続
$tank1 = new Tank('localhost1', 'user1', 'password', 'scheme1');
// ホスト名、ユーザー名、データベース名が同じなのでリソースは共有される
$tank2 = new Tank('localhost1', 'user1', 'password', 'scheme1');
// ユーザー名が同じでは無いので新規接続される
$tank3 = new Tank('localhost1', 'user2', 'password', 'scheme1');

target()で使用するテーブルを指定します。テーブルを指定するとテーブルの情報を自動的に読み込んで保持します。テーブルを指定しないとArrmy\Tankは使用できません。

また、テーブルを指定した後に別のテーブルを指定することも出来ます。

<?php
$tank->target('table1');
$tank->target('table2');

この場合は新しく指定した「table2」の情報を保持し、「table1」の情報は破棄されます。複数のテーブルを扱う場合は別々のインスタンスを作成します。

<?php
$tank1 = new Tank('localhost', 'user', 'password', 'scheme');
$tank1->target('table1');
$tank2 = new Tank('localhost', 'user', 'password', 'scheme');
$tank2->target('table2');

以上でArrmy\Tankを使用する準備は整いました。ここからは下記テーブルの情報を保持した状態のオブジェクト「$tank」があるとして説明します。

CREATE TABLE `table` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `data` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

SELECT

テーブルから特定のレコードを取り出すには以下の様にします。

<?php
$result = $tank[123];

この処理で発行されるSQLは、

SELECT * FROM `table` WHERE `id` = 123;

で、$resultにはデフォルトでこのSQLを実行したmysqli_fetch_assocの結果が入ります。

自動的にプライマリーキーを判断して配列のオフセットからSQLを発行します。複合キーのテーブルの場合はCREATE TABELした時の先に設定されているキーで発行されます。

プライマリーキー以外のカラムを指定してSELECTするには以下の様にします。

<?php
$params = array('data' => 'abc');
$result = $tank[$params];
SELECT * FROM `table` WHERE `data` LIKE 'abc';

複数カラムを指定する事もできます。複数指定した場合はAND検索になります。

<?php
$params = array('id' => 123, 'data' => 'abc');
$result = $tank[$params];
SELECT * FROM `table` WHERE `id` = 123 AND `data` LIKE 'abc';

オブジェクトで指定することも出来ます。

<?php
class Hoge{}
$hoge = new Hoge();
$hoge->id = 123;
$hoge->data = 'abc';
$result = $tank[$hoge];
SELECT * FROM `table` WHERE `id` = 123 AND `data` LIKE 'abc';

SQLの実行結果が2行以上だった場合は2次元配列で結果が取得されます。

INSERT

データを登録するには配列に要素を追加するように指定します。

<?php
$tank[] = array('data' => 'abc');

これを実行すると下記のSQLが発行されます。

REPLACE INTO `table` (`data`) VALUES ('abc');

プライマリーキーを指定した場合、

<?php
$tank[] = array('id' => 123, 'data' => 'abc');
REPLACE INTO `table` (`id`, `data`) VALUES (123, 'abc');

REPLACE文でSQLが発行されるので、同じキーのレコードが存在した場合は上書されます。

array_pushではSQLは発行されず何も起こりません。

<?php
// 何も起きない
array_push($tank, array('data' => 'abc'));

こちらもオブジェクトを指定することが出来ます。

<?php
class Hoge{}
$hoge = new Hoge();
$hoge->id = 123;
$hoge->data = 'abc';
$tank[] = $hoge;
REPLACE INTO `table` (`id`, `data`) VALUES (123, 'abc');

UPDATE

データをアップデートするには先程のREPLACEの方法を使うか、以下の様にします。

<?php
$tank[123] = array('data' => 'def');

プライマリーキーが自動で判別され、以下のSQLが発行されます。

UPDATE `table` SET `data` = 'def' WHERE `id` => 123;

SELECT同様複合キーだった場合は先に設定されているキーで発行されます。

プライマリーキー以外のカラムを指定してUPDATEする事もできます。

<?php
$params = array('data' => 'abc')
$tank[$params] = array('data' => 'def');
UPDATE `table` SET `data` = 'def' WHERE `data` LIKE 'abc';

SELECT・INSERT同様にオブジェクトを指定することも出来ます。

<?php
class Hoge{}
$hoge = new Hoge();
$hoge->id = 123;
$hoge->data = 'abc';
$piyo = new Hoge();
$piyo->data = 'def';
$tank[$hoge] = $piyo;
UPDATE `table` SET `data` = 'def' WHERE `id` => 123 AND `data` LIKE 'abc';

DELETE

DELETEするにはunsetするか、NULLを代入します。

<?php
unset($tank[123]);
<?php
$tank[123] = null;
DELETE FROM `table` WHERE 'id' => 123;

こちらもプライマリーキー以外のカラムが使用でき、オブジェクトで指定することができます。

<?php
class Hoge{}
$hoge = new Hoge();
$hoge->data = 'abc';
unset($tank[$hoge]);
DELETE FROM `table` WHERE 'data' LIKE '123';

SQLを直接実行

複雑なSQLなどは直接書いて実行することも出来ます。

<?php
$result = $tank->charge('SELECT * FROM `table`')->fire()->fetchAll();

chargeでSQLを登録し、fireでSQLを実行します。その後fetchAllでSQLの全結果を取得することが出来ます。

fetchAllでデータを取得すると実行結果データをすべて格納することになるので、大規模データを扱う際はメモリを消費してしまうのであまりオススメできません。

以下のようにSQLを実行した後にforeachでデータを取得すればストリーミング的にデータが取得できますので、メモリを消費してしまう心配はありません。

<?php
$result = $tank->charge('SELECT * FROM `table`')->fire();
foreach($tank as $key => $row)
{
    echo $row['data'];
}

また、chargeメソッドはプリペアドステートメントを使用して、SQLを設定する事も出来ます。

<?php
$query = 'UPDATE `table` SET `data` = \'?\' WHERE `id` => ?';
$params = array('def', 123);
$tank->charge($query, $params)->fire();

上記のようにすると、配列の順で「?」が置換され、以下のSQLが発行されます。

UPDATE `table` SET `data` = 'def' WHERE `id` => 123;

動的にメソッドを登録

動的にメソッドを登録することが出来ます。メソッドを登録するにはaddOperationでメソッド名、処理内容のコールバックを指定します。

よく使うSQLなどを以下の様に登録しておいて呼び出すと便利です。

$tank->addOperation('hoge', function()
{
    $this->charge('SELECT * FROM `table` WHERE `id` = 1')->fire();
    return $this;
});
$tank->addOperation('piyo', function()
{
    $this->charge('SELECT * FROM `table` WHERE `id` = 2')->fire();
    return $this;
});

foreach($tank->hoge() as $key => $row)
{
    echo $row['id'];
}
foreach($tank->piyo() as $key => $row)
{
    echo $row['data'];
}

データの取得方法を変更する

SELECT文の実行結果はデフォルトで配列(mysqli_fetch_assocの結果)で取得されますが、setFetchCallbackを使用して取得結果を変更することが出来ます。

class Hoge{}
$tank->setFetchCallback(function()
{
    $hoge = new Hoge();
    foreach($this->row as $key => $value)
    {
        $hoge->$key = $value;
    }
    return $hoge;
});

$result = $tank->charge('SELECT * FROM `table`')->fire();
foreach($tank as $key => $row)
{
    echo $row->id;
    echo $row->data;
}

setFetchCallback内で使用できる$this->rowにはデフォルトのmysqli_fetch_assocの結果が入っております。

setFetchCallbackで設定した処理は、全行取得した後にクリアされてしまいます。二回目以降も続けて使用する場合はsetFetchBaseCallbackを使用します。

class Hoge{}
$tank->setFetchBaseCallback(function()
{
    $hoge = new Hoge();
    foreach($this->row as $key => $value)
    {
        $hoge->$key = $value;
    }
    return $hoge;
});

$result = $tank->charge('SELECT * FROM `table`')->fire();
foreach($tank as $key => $row)
{
    echo $row->id;
    echo $row->data;
}

// resetFetchBaseCallbackで元に戻ります。
$tank->resetFetchBaseCallback();

工場を作って更に便利に

以下のようにSingletonパターンのクラスを用意してインスタンスを作成する様にしておくことで、

  • いちいちDBサーバーへの接続先を指定する手間を省く
  • テーブル単位で、一度作成したインスタンスはどこからでも呼び出し可能

という便利な使い方ができます。

require_once 'Arrmy' . DIRECTORY_SEPARATOR . 'Tank.php';
use Arrmy\Tank;

class Factory
{
    private static $_instance = null;

    private $_tanks = array();

    private function __construct()
    {
    }

    public static function getInstance()
    {
        if(is_null(self::$_instance))
        {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function tank($target)
    {
        if(!isset($this->_tanks[$target]))
        {
            $tank = new Tank('localhost', 'user', 'password', 'scheme');
            $tank->target($target);
            $this->_tanks[$target] = $tank;
        }
        return $this->_tanks[$target];
    }
}

次の様に呼び出して使用することが出来ます。

$tank = Factory::getInstance()->tank('table');

その他・注意点等

SQLを実行しないでforeachで回すと全行取得処理になります。

<?php
// SELECT * FROM `table`の実行結果と同等
foreach($tank as $key => $row)
{
}

countで取得できるのはテーブルの全レコード数になります。

<?php
echo count($tank);

Arrmyはmysqli_use_resultを使用してSQLの結果を取得しているため、結果をすべて取得する前に次のSQLを実行するとエラーになってしまいます。

<?php
$tank->charge('SELECT * FROM `table`)->fire();
foreach($tank as $key => $row)
{
    break;
}
// ここでエラー
$tank->charge('SELECT * FROM `table`)->fire(); 

SQLの実行結果をすべて取得せずに次のSQLを実行する場合は、clearを使用して結果セットを開放することでエラーを回避できます。

<?php
$tank->charge('SELECT * FROM `table`)->fire();
foreach($tank as $key => $row)
{
    break;
}
// 結果セット開放
$tank->clear();
// 次のSQLを実行
$tank->charge('SELECT * FROM `table`)->fire(); 

やろうと思っていた事は出来たけど

やろうと思っていたことは出来た感じ。

だけど本当に使いやすいかは、、、わからない。

時間があったらなんかで使ってみて、機能追加とかしていこうと思っています。

コメントを残す