日記
Zend Frameworkをどう使うか その14Edit

O/Rマッパー編その2。Zend_Db_Tableのfetch系メソッドで返されるZend_Db_Table_RowおよびZend_Db_Table_Rowsetについて。名前を見れば分かるとおり、Zend_Db_Table_Rowが1行レコードに相当するクラスで、Zend_Db_Table_Rowsetが複数行のレコードを管理するクラスね。

Zend_Db_Table_Rowsetは、単に内部にZend_Db_Table_Rowオブジェクトの配列を抱えているだけで、DBMSレベルでのカーソルとかは使っていない。「100万行返すZend_Db_Table::fetchAllを投げたら、100万個のZend_Db_Table_Rowオブジェクトを作ったるでぇ」という気合いの入った仕様になっている。

Zend_Db_Table_Rowsetのメソッドは、以下のような感じ。

  • current() - カレントのZend_Db_Table_Rowオブジェクトを返す。存在しない場合はfalseが返る。
  • next() - ポインタを次の行に移動し、新しいポインタを返す。
  • rewind() - ポインタを先頭に移動。
  • valid() - 有効なポインタを差しているか。
  • count() - レコード行数。
  • exists() - 有効なレコードを持つか(空のZend_Db_Table_Rowsetかどうか)
  • toArray() - 保持している全行のデータを、配列+連想配列形式(旧世代のPHPらしいデータ形式)に変換して返す。

ただし、ほとんどiteratorインターフェースを実装するためのメソッドね。ふつうのクラスライブラリ的に書けば、

$foo = new Foo(); // Zend_Db_Tableオブジェクト
$rows = $foo->fetchAll(); // 全行取得
while ($rows->valid()) {
$row = $rows->current();
// 各行に対する処理
$rows->next();
}

なんて感じになるけど、iteratorを使えば、

$foo = new Foo(); // Zend_Db_Tableオブジェクト
$rows = $foo->fetchAll(); // 全行取得
foreach ($rows as $row) {
// 各行に対する処理
}

なんて書き方になる。PHPだったらこっちの書き方の方がふつうかな。

そうやって取得した1レコード単位のZend_Db_Table_Rowのメソッドは、以下のような感じ。

  • save() - primary keyがセットされていればupdate、セットされていなければinsertを行う。
  • toArray() - データを連想配列として返す。
  • setFromArray(array $data) - 連想配列から各カラムの値をセットする

なんて感じ。もちろん各カラムの代入と参照は、上記メソッド以外にも、

$row->foo = 1;
$row->bar = 'BAR';
$row->save();

なんて感じで、__set、__getメソッドを経由したプロパティ表現でも書くことができる。

以上でZend FrameworkのO/Rマッパー周りの機能は一通り見ていったわけだけど、きれいな仕様ではあるけれども、かなり機能は少ないなーというのが、全体を通しての印象。

特にO/Rマッパーは1テーブル単位の操作でしか(検索でさえも)使えないって仕様は、結構微妙な気がする。というのはDB_DataObjectの、検索だけなら複数テーブルをまたいだ処理にもデータオブジェクト互換の操作が使える、という仕様に俺が慣れてしまっているからかもしれないけど。具体的には、

$stock = new StockTable(); // 在庫テーブル
$product = new ProductTable(); // 商品テーブル
$product->kind = 'book'; //種類は「本」
$stock->joinAdd($product); // 在庫テーブルと商品テーブルをjoin
$stock->find();
while ($stock->fetch()) {
if ($stock->amount == 0) {
// 在庫がない本の場合の処理
}
}

みたいな書き方がDB_DataObjectでは簡単にできたわけだけど、これをZend_Dbを使って書くと

$select = $db->select();
$select->from('stock', '*');
$select->join('product', 'stock.product = product.id', '*');
$select->where('product.kind = ?', 'book');
$sql = $select->__toString();
$rows = $db->fetchAll($sql);
foreach ($rows as $row) {
if ($row['amount'] == 0) {
// 在庫がない本の場合の処理
}
}

みたいにZend_Db_Adapter+配列による結果セットを使って書くか、あるいは、

$select = $db->select();
$select->from('product', 'id');
$select->where('kind = ?', 'book');
$sql = $select->__toString();
$rows = $db->fetchAll($sql);
$ids = array();
foreach ($rows as $row) {$ids[] = $row['id'];}
class Stock extends Zend_Db_Table {};
$stock = new Stock();
$rows = $stock->fetchAll($db->quoteInto('product in (?)', $ids));
foreach ($rows as $row) {
if ($row->amount == 0) {
// 在庫がない本の場合の処理
}
}

みたいに回りくどくZend_Db_Tableを使って書くことになるんだよね? なんかDB_DataObjectの頃よりもずいぶんわかりにくくなっちゃったなーと思えてしまう。それとも俺が思いついていないだけで、現状のZend_Dbを使ってもっときれいに書く方法があるのかな?

一応補足しておくと、

Zend_Db_Adapterを使って、連想配列で結果を受け取った場合、たとえばamountというカラム名を間違って、

    if ($row['amaunt'] == 0) {

なんて書いたとしても、(E_NOTICEが有効ならばnoticeは出るけど)エラーは出ない。一方、Zend_Db_Table_Rowで結果を得る場合は、

    if ($row->amaunt == 0) {

と書くと、存在しないカラム名を使ったと言うことで、例外が投げられる。もちろん後者の方がコーディングスタイルとしてはいいよね。

Published At2006-03-24 00:00Updated At2006-03-24 00:00