PDOでO/Rマッピング

前回,PDOを使ってみた で,PDOを使ってSQLiteデータベースにアクセスする例を示しました.そこで,memberテーブルを操作するMemberクラスなるものを作りましたが,Memberクラスが肥大化してしまっており,これをなんとかしたいのです.

これはゼミで私がPDOを取り上げたのがキッカケで,PDOについて説明するついでに,なんか便利なクラスを作っちゃえーということで,いつもは手続き指向でPHPをちょこまか書いている私ですが,PHPのオブジェクト指向も体感してみるか,という軽い気持ちで取り組んだらハマってしまったという類いのものです.

Memberクラス(以前の状態)

別のテーブル,例えば students テーブルか何かを作成した時に,また,find()メソッドやらsave()メッソドやらを実装するのは非効率的.

そこで,汎用クラスいわゆるMemberクラスのスーパークラスを考えます.ここでは,かっこ良く(?)「PDOModel」としました.find()メソッドやsave()メソッドは,ここに実装することになります.しかし,save()メソッドは,INSERT文やUPDATE文を使用しますので,フィールド名の情報が必要です.ですのでサブクラスとなるMemberでフィールド名の情報,ここでは $fields というフィールド名をキーとし値をデータ型名とする配列を利用します.後,必要な情報としてはクラス名とテーブル名ですので,それも定数として定義しておきます.

Memberクラスと汎用クラス

ご注意として,今回はとりあえず動けば良いという考えのもとシンプルな実装にしています.ですので,例外処理とか気にせずに記載していますので,これをそのまま業務アプリに適用しようとすると問題があります.業務アプリ等は既に出回っているフレームワークをご利用頂ければと思います.

ここで使用しているソースは,こちらに置いています.
tamochia/pdo_model | GitHub

今回はあくまでPHPでなんちゃってActiveRecordを実装してみようっていうだけのことですから,はい...

member_test.php ー Memberクラスをテストするプログラム

PDOModelクラス,Memberクラスをテスト(利用)するプログラムとしては,次のようなイメージで考えています.

// bootstrap
PDOModel::configuration('pdo_config.xml');
PDOModel::connection();

// ID:0002のレコードを更新する
$obj = Member::find('0002');
$obj->name = 'Hayato Satsuma';
$obj->height = '168.0';
$obj->save();

// ID:0005のレコードを削除する
$obj = Member::find('0005');
$obj->delete();

// heightが170より大きいレコードを取得
$obj = Member::where('height > 170');
print_r($obj);

// 新規レコードを追加する
$obj = new Member();
$obj->id = '0005';
$obj->name = 'Takamori Saigo';
$obj->height = '169.5';
$obj->weight = '50.9';
$obj->save();

// 全レコードを取得
$obj = Member::findAll();
print_r($obj);

細かく見て行きます.まずは,データベースとの接続の部分から.

// bootstrap
PDOModel::configuration('pdo_config.xml');
PDOModel::connection();

この2行で,MySQLデータベースなりSQLiteデータベースなりに接続し,PDOハンドルを取得させます.

configuration()メソッドでは,コンフィグXMLファイルを指定することで,使用するデータベースを汎用的にセレクトさせるようにします.ここでいう「pdo_config.xml」の中身は,こんな感じです.

<?xml version="1.0" encoding="UTF-8"?>
<config>
	<dsn>sqlite:./sample.sqlite3</dsn>
	<user></user>
	<password></password>
</config>

MySQLの場合はこんな感じ.

<?xml version="1.0" encoding="UTF-8"?>
<config>
	<dsn>mysql:dbname=sample;host=localhost;charset=utf8</dsn>
	<user>foo</user>
	<password>hogehoge</password>
</config>

データベースを移行した際は,このコンフィグXMLファイルを入れ替えるだけで良いことになります.

connection()メソッドで,データベースと接続して,PDOオブジェクト生成するのですが,そこは内部的に処理して,クラス外では見せなくしています.

任意のレコード,例えばid=’0002’のレコードのデータを変更(更新)する際は,まず,find()メソッドで目的のレコードオブジェクトを取得してから,そのオブジェクトのプロパティ値を変え,最後にsave()メソッドでデータベースに反映させます.

// ID:0002のレコードを更新する
$obj = Member::find('0002');
$obj->name = 'Hayato Satsuma';
$obj->height = '168.0';
$obj->save();

削除する際も同様で,まずfind()メソッドで目的のオブジェクトを取得してから,delete()メソッドで削除させます.

// ID:0005のレコードを削除する
$obj = Member::find('0005');
$obj->delete();

ある条件,例えばWHERE句の部分を記述することにより,汎用的に目的のレコード群(結果セット)を取得したい場合は,where()メソッドを使用します.

// heightが170より大きいレコードを取得
$obj = Member::where('height > 170');
print_r($obj);

新規にレコードを追加する場合は,newでMemberインスタンスを生成し,それから各プロパティ値をセットし,最後にsave()メソッドを実行させます.

// 新規レコードを追加する
$obj = new Member();
$obj->id = '0005';
$obj->name = 'Takamori Saigo';
$obj->height = '169.5';
$obj->weight = '50.9';
$obj->save();

もちろんfindAll()メソッドも準備します.

// 全レコードを取得
$obj = Member::findAll();
print_r($obj);

それでは,まずMemberクラスの実装から...

<?php                                                                                                                                                   
require_once "pdo_model.php";

class Member extends PDOModel {
    const CLASS_NAME = 'Member';
    const TABLE_NAME = 'member';
    protected static $fields = array (
        'id' => PDO::PARAM_STR,
        'name' => PDO::PARAM_STR,
        'height' => PDO::PARAM_STR,
        'weight' => PDO::PARAM_STR
    );
}
:

PDOModelクラスは,Memberクラスのスーパークラスです.PDOModelの方で,サブクラス名を既存メソッドか何かで取得させたかったのですが,やり方がわからなかったので,CLASS_NAME という定数で渡しています.同じ様にテーブル名も必要なので,TABLE_NAME という定数も用意しました.
PDOModelクラスの方で,bindParam()メソッドを利用しますので,フィールド名とその型の情報が必要となります.そこで,$fieldsというstaticな配列プロパティも用意しました.

pdo_model.php ー PDOModelクラス

後は,PDOModelクラスの実装となります.まずは,用意するプロパティは次のような感じ.これも色々精査する必要があるんだけど,とりあえず動けば良いという前提で今回はTRYしています.

<?php                                                                                                                                                   
class PDOModel {
    private static $db;  // PDOのハンドル
    private static $table;  // テーブル名
    private static $pdo_params;  // PDOコンストラクタのパラメータ
    public $id;

次に,各メソッドを見て行きます.まずは,データベースとの接続に関するクラスメソッドから.

    // DB設定XMLファイルの読み込みとパラメータセット
    public static function configuration($xml) {
        $conf = simplexml_load_file($xml);
        self::$pdo_params = get_object_vars($conf);
    }

    // データベースへの接続,PDOインスタンスの生成
    public static function connection() {
        $_dsn = self::$pdo_params['dsn'];
        $_user = self::$pdo_params['user'];
        $_password = self::$pdo_params['password'];
        try {
            self::$db = new PDO($_dsn, $_user, $_password);
        } catch(PDOException $e) {
            printf("Error: %s\n", $e->getMessage());
            self::$db = null;
        }
    }

ここで注目すべきは,configuratio()メソッドのXMLパースの部分.本当はRailsのようにYAMLファイルをコンフィグファイルとして利用したかったのですが,PHPのYAMLパースは標準で実装されていない(?)ぽかったので,XMLとしました.でもsimplexml_load_file()メソッドを使うだけで良かったです.そこで取得したデータを $pdo_params 配列に格納し,その情報を connection() メソッドで利用します.

	// 結果セット,レコードオブジェクトの配列を返す
	protected static function getRecords(PDOStatement $stmt) {
		$rets = array();
		while($ret = $stmt->fetchObject(static::MODEL_CLASS)) $rets[] = $ret;
		return $rets;
	}
	
	// 単一のレコードオブジェクトを返す
	protected static function getRecord(PDOStatement $stmt) {
		$ret = $stmt->fetchObject(static::MODEL_CLASS);
		return $ret;
	}

	// すべてのレコードオブジェクト配列を返す
	public static function findAll() {
		$sql = "SELECT * FROM ".static::TABLE_NAME;
		$stmt = self::$db->prepare($sql);
		$stmt->execute();
		return self::getRecords($stmt);
	}

	// 任意のidのレコードオブジェクトを返す
	public static function find($id) {
		$sql = "SELECT * FROM ".static::TABLE_NAME." WHERE id = :id";
		$stmt = self::$db->prepare($sql);
		$stmt->bindParam(':id', $id, PDO::PARAM_STR);
		$stmt->execute();
		return self::getRecord($stmt);
	}

	// 任意のWHERE検索にてレコードオブジェクト配列を返す
	public static function where($cond) {
		$sql = "SELECT * FROM ".static::TABLE_NAME." WHERE ".$cond;
		$stmt = self::$db->query($sql);
		return self::getRecords($stmt);
	}
	
	// 対象オブジェクトレコードの削除
	public function delete() {
		$sql = "DELETE FROM ".static::TABLE_NAME." WHERE id = :id";
		$stmt = self::$db->prepare($sql);
		$stmt->bindParam(':id', $this->id, PDO::PARAM_STR);
		$stmt->execute();
	}	

ここは,前回の PDOを使ってみた で説明したとおりで,そのまま実装しています.delete()メソッド以外はすべてクラスメソッドとしています.delete()メソッドはインスタンスメソッドになりますので,WHERE句でのid特定については,69行目のbindParamの第2引数にて「$this->id」を持ってきています.

工夫が必要だったのは次のsave()メソッドの部分です.

	// 対象オブジェクトレコードの保存(新規及び更新)
	public function save() {
		$obj = self::find($this->id);
		if($obj == null) {
            // 新規レコード追加(存在しないidの場合)
			$flist = implode(array_keys(static::$fields), ",");
			$vfunc = function($v){return(":".$v);};
			$vlist = implode(array_map($vfunc, array_keys(static::$fields)), ",");
			$insert_sql = "INSERT INTO ".static::TABLE_NAME." (".$flist.") VALUES (".$vlist.")";
			$stmt = self::$db->prepare($insert_sql);
		}
		else {
            // 既存レコードの更新
			$sfunc = function($v){return($v."=:".$v);};
			$slist = implode(array_map($sfunc, array_keys(static::$fields)), ",");
			$update_sql = "UPDATE ".static::TABLE_NAME." SET ".$slist." WHERE id=:id";
			$stmt = self::$db->prepare($update_sql);
		}
		foreach(static::$fields as $key => $value)
			$stmt->bindParam(":".$key, $this->{$key}, $value);
		$stmt->execute();
	}	
}
?>

bindParamメソッドでは,テーブルの各フィールド名の情報が必要です.そこで,サブクラスとなるMemberクラスの$fieldsプロパティからフィールド名とそのデータ型を取得します.それには,array_keys(static::$fields) を利用します.

しかし,INSERT文のVALUES句において,

VALUES(:id, :name, :height, :weight)

のように,「コロン+フィールド名」のカンマ付きリスト文字列が必要です.それには,次のようにarray_map()関数を使って作成しています.

$vfunc = function($v){return(":".$v);};
$vlist = implode(array_map($vfunc, array_keys(static::$fields)), ",");

また,UPDATE文のSET句おいては,

SET id = :id, name = :name, height = :height, weight = :weight

という「フィールド名+”=:”+フィールド名」のカンマ付きリスト文字列が必要ですので,先述と同様にarray_map()関数を使って次のように作成しました.

$sfunc = function($v){return($v."=:".$v);};
$slist = implode(array_map($sfunc, array_keys(static::$fields)), ",");

無理矢理感がありますが,とりあえず短い記述になるようまとめました.これですべてのテーブルに対応できるようにしています.

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中