blog.ishinao.net

243月/06

Zend Frameworkをどう使うか その15

もう一個触れていなかったZend_Db周りのクラスがあったんで、それについても一応書いておこう。

Zend_Db_Profilerってクラスがそっと存在していて、よく見るとZend_Db_Adapterがそのオブジェクトを持っている。名前を見れば分かるとおり、DBクエリーのプロファイリングをしてくれるクラスね。

これはZend_Db_Adapterを生成するときに、

$config = array(
..., //DB接続設定は省略
'profiler' => true,
);
$db = Zend_Db::factory('pdo_mysql', $config);

なんて感じで、profiler = trueな設定を渡すと有効になる。あるいは、生成したZend_Db_Adapterオブジェクトから、

$profiler = $db->getProfiler();
$profiler->setEnabled(true);

なんてやっても有効になる。

プロファイラーが有効な状態で実行されるすべてのクエリーは、その実行時間がプロファイラー内部で記録されていく。

$profiles = $profier->getQueryProfiles();
foreach ($profiles as $profile) {
echo 'クエリー: ' . $profile->getQuery() . "\n";
echo 'クエリー種別: ' . $profile->getQueryType() . "\n"; // Zend_Db_Profiler内で定義
echo '実行時間: ' . $profile->getElapsedSecs() . "\n"; //micro秒単位まで
}

なんて感じで取得できる模様。

必要なところだけプロファイラーを有効にしたりできるから、ベンチマークを取りたいところだけで有効にして、その結果をログとかに吐き出したりすると、いいかもしれない。MySQLのSlow Query Log相当の機能を自前で作ったりも簡単にできそうだね。

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その15 Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

Zend Frameworkをどう使うか その14

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) {

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

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その14 Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

Zend Frameworkをどう使うか その13

「ようやくO/Rマッパーにたどり着きましたよ」のZend_Db_Table。使い方は、

// $db - Zend_Db_Adapterオブジェクト
Zend_Db_Table::setDefaultAdapter($db);
class Foo extends Zend_Db_Table {} // fooテーブルに対応するクラスの宣言
$foo = new Foo(); // $fooテーブルに対応したオブジェクト

って感じ。実行環境(static protected Zend_Db_Table::$_defaultDb)にDB接続情報をセットしておくってやり方は、DB_DataObjectがPEARのstaticPropertyを使っていたやり方と一緒だね。

ただし、上記のようにオプション設定なしでテーブルクラスを宣言した場合は、

  • クラス名 = テーブル名
  • テーブルのプライマリーキーとなるカラム名 = id

となっている必要(規約)がある。その辺を変更したい場合は、

class Table_Foo extends Zend_Db_Table
{
protected $_name = 'foo'; // テーブル名
protected $_primary = 'id'; // プライマリーキーとなるカラム名
}

なんて感じでクラスを定義すればいい*1。実際に使うときにはどう書くんだろうなー。適当なprefixをつけてクラス定義ファイルを自動生成しつつ、必要に応じて内容を修正する感じになるのかなー。まだ具体的な利用イメージは湧かない。

Zend_Db_Tableオブジェクトに対してできる操作は、

  • getAdapter() - Zend_Db_Adapterオブジェクトが取得できるんで、細かい操作が必要な場合はこれ経由で処理。
  • info() - テーブル情報が取得できる。テーブル名、カラム名一覧、プライマリーキーカラム名の三つを連想配列で。
  • insert(array $data) - 連想配列でデータを渡してinsertを実行。やっぱり内部的にはZend_Db_Adapter::insertが呼ばれているね。
  • update(array $data, string $where) - 連想配列でデータを、SQL文でwhere条件を渡してupdateを実行。こっちも内部的にはZend_Db_Adapter::updateが呼ばれている。
  • delete(string $where) - SQL文でwhere条件を渡してdeleteを実行。
  • find(mixed $val) - primary keyに対する検索。$valにscalarを渡すと1行検索(戻り値はZend_Db_Table_Row)、配列を渡すとin検索(戻り値はZend_Db_Table_Rowset)される。
  • fetchAll($where, $order, $count, $offset) - ふつうにselect発効。引数はすべて省略可能。戻り値はZend_Db_Table_Rowset。
  • fetchRow($where, $order) - 1行select。複数行が選択されるような$whereの場合は最初の1行。ただし、Zend_Db_Table_Rowにはnextみたいなメソッドはないから、2行目以降に移動することはできない。
  • fetchNew() - 空のZend_Db_Table_Rowを返す。Zend_Db_Table::insertではなく、Zend_Db_Table_Row::saveを使ってinsertしたいときに使うんだろう。

なんて感じ。クラス構成としては、Zend_Db_Tableはテーブル全体に対する操作を行い、レコード単位の操作は、Zend_Db_Tableのfetch系メソッドで返される、Zend_Db_Table_Row(set)で行うというイメージね。

Zend_Db_Table_Row(set)の話は後回しにして、Zend_Db_Tableの拡張の話をもうちょっと続ける。

最初の方で、テーブル名やプライマリーキーの設定を変更するためのクラス定義の話を書いたけど、それ以外にもクラスを拡張する方法はある。つっても、単にふつうのクラスの拡張の仕方と一緒で、Zend_Db_Tableを継承したクラスで、メソッドを上書き(override)するなり追加するなりすれば、機能拡張できますよ、という話。

たとえば、

class Foo extends Zend_Db_Table
{
function insert($data)
{
if (empty($data['created']) {$data['created'] = time();}
parent::insert($data);
}
}

なんて感じで、insert時に生成時間を自動的に更新したり、

class Foo extends Zend_Db_Table
{
function findByName($name)
{
$db = $this->getAdapter();
$where = $db->quoteInto('name = ?', $name);
return $this->fetchAll($where);
}
}

なんて感じで独自の検索メソッドを追加したりって感じか。でもこの例なんて、DB_DataObjectだったら標準クラスを使って、

$foo = new FooTable(); // DB_DataObjectによるfooテーブルオブジェクト
$foo->name = $name;
$foo->find();

で済んだんだけどね。

*1 他の書き方もあるけど

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その13 Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

Zend Frameworkをどう使うか その12

もちろんこんなことになるなら、連番ではなくちゃんとしたセクションタイトルを付けるべきだったと後悔しているわけですよ。またZend_Dbネタの続きで、今度はselectクエリー生成ユーティリティクラスであるZend_Db_Selectクラスについて。

第一印象としては、Zend_Db_Selectに関しても、Zend_Db_Adapter::insertとかZend_Db_Adapter::updateとかと同様に、微妙に中途半端な感じがする。ちなみにZend_Db_Selectクラスの具体的な使い方としては、

// $db - Zend_Db_Adapterオブジェクト
$select = $db->select();
$select->from('tbl', '*'); // select * from tbl
$select->where('foo = ?', 1); // where foo = 1
$select->order('bar'); // order by bar
$select->limit(10, 5); // MySQLなら limit 10, 5
$sql = $select->__toString(); // select * from tbl where foo = 1 order by bar limit 10,5

って感じ。ちなみに上記サンプルには使ってないけど、他にもgroupとかhavingとかSQL文法要素のパターン一通り*1のメソッドがあって、メソッド名から想像したとおりに動く。

なんかもう「うわー」って感じがしない? こういうユーティリティクラスはそれなりに便利だとは分かる(昔自分でも作ったよ)けど、O/Rマッパー全盛の時代にこれをメインに使うのはちょっとビミョーな気がする。

ただ、こういうクラスが作られた理由は何となく想像できる。

というのは、後で触れるZend_Db_TableによるO/Rマッパー機能には、PEAR DB_DataObjectには存在したデータオブジェクト同士のjoin機能がなくなっている。DB_DataObjectでは、[dbname].links.iniにリンク情報を記述しておけば、

$foo = new FooTable(); // DB_DataObjectによるfooテーブルオブジェクト
$bar = new BarTable(); // DB_DataObjectによるbarテーブルオブジェクト
$foo->joinAdd($bar); // links.iniの設定に基づいて二つのテーブルをjoinする
$foo->find(); // select文を発行

なんて感じで、joinを使ったselect文をデータオブジェクト操作のレベルで書けたけど、Zend_Db_Table(_Row)からはそういう機能はすっぱり削除された。実際にはもちろんjoinを使ったselectなんて山のように使うわけで「O/Rマッパーのレベルでのjoin機能を削った代わりに、joinを伴う複雑なselectの管理はこっちでやってね」というんで作られたのが、Zend_Db_Selectなんじゃなかろうか。

まあ確かに、DB_DataObjectのjoin機能は結構微妙なできではあったし*2、その完成度を高めるってのはかなり難しい*3ことは分かるけど、こうなっちゃうと使い勝手としてはDB_DataObjectから後退している気がするんだよなー。単体ではきれいな仕様&実装ではあると思うけど。

ちなみにZend_Db_Selectを使ったjoinのやり方は、こんな感じになる。

$select = $db->select();
$select->from('foo', '*'); // select foo.* from foo
$select->join('bar', 'foo.id = bar.id', '*'); // select bar.* ... join bar on foo.id = bar.id
$sql = $select->__toString(); // select foo.*, bar.* from foo join bar on foo.id = bar.id

うーん、俺なら素直にSQL文+placeholderで書いちゃう気がするなー。Zend_Db_Selectを使っても、結局O/RマッパーであるZend_Db_Tableとは異なるオブジェクトに対する操作になってしまうし、だったらO/Rマッパーとは異なる形でSQL文を隠蔽するZend_Db_Selectを使うよりは、まだSQL文を直接書いた方がわかりやすい気がする。Zend_Db_Selectを使う場合は、生成されるSQL文を完全に頭の中にイメージしていないと使えないわけだし。

あ、一点Zend_Db_Selectを使う確実な利点があったな。limit関係のSQLはDBMSによっていろいろ異なるんだけど、それをZend_Db_Selectを使うことで吸収してくれる模様。それが重要ならば使う価値があるかもね。

*1 +追加条件の場合はorWhereとかorHavingとかの論理演算指定含む

*2 実用レベルでは、内部的に生成されるSQL文を意識しながら使う必要があったり

*3 DBスキーマ側まで規約で縛ったりすれば何とかなるかもしれないけど

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その12 Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

オランダ語?

Zend FrameworkのMLにZend_View_Smartyの件でメールを投げてみたら、すぐにレスポンスメールが返ってきたんだけど、なにやら読めない言語で書いてある。.nlドメインってオランダ? ドイツ語とちょっと似ているけど、やたらと母音を連続する単語が多いな。文中に含まれる日付らしき要素(これは英語と似ているからだいたいわかる)から察するに、多分自動応答の不在通知メールだろう。まあMLでは良くあることだけど(文字化けではなく)読めない言語で返ってくるとなかなか焦る。

このエントリーを含むはてなブックマークはてなブックマーク - オランダ語? Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

Zend Frameworkをどう使うか その11

Zend_Dbの続き。

Zend_Db_Adapterは、エスケープが書きやすくなったところがPEAR DBと比べての一番大きな違いかなー。メソッド名がわかりやすくなって、PEAR DBでは

$db->autoCommit(false);

だったのが、

$db->beginTransaction();

とふつうに書けるようになってたりするのも、可読性が上がっていいと思うけど。

ちなみにqueryメソッドで直接SQL文を渡す方法以外に、insertやupdateメソッドでは、

$table = 'tbl';
$row = array('foo' => 1, 'bar' => 'BAR');
$db->insert($table, $row); // insert into tbl(foo, bar) values(1, 'BAR');

みたいな書き方ができるみたいだけど、この書き方はうれしいかどうかちょっと微妙。PHPコードだけを見たら、それなりの利点(コードの見やすさ)は感じるんだけど、このPHPコードがDBに対してどういう操作をするのか、いまいちわかりにくい。

O/Rマッパーくらい突き抜けちゃえば、その利点の大きさは認めるけど、こういう中途半端な形でSQL文の隠蔽を行うくらいならば、ふつうにplaceholderとかを使ったSQL文へのパラメータ埋め込みを書いた方が、トータルでの可読性は高いんじゃなかろうか。

っつーかまあ、どうせZend_Dbには標準でO/Rマッパーもあるから、この中途半端な記法は、本来はO/Rマッパー機能の中の人(メソッド)が使うために用意されているんだろうけどね。

ちなみにZend_Db_Adapter周りの機能は、Zend_Db_Adapter+Zend_Db_Statement(PDOを利用する場合は、PDOStatementがそのまま使われる)の組み合わせで実現されているんで、Zend_Db_Adapter周りを一通り知りたければ、本当はZend_Db_Statementの方も追う必要がある。PDOがPDOクラスとPDOStatementクラスの二つから構成されているのと、相似関係にあるわけだね(ってことで、前に書いた多分Zend_Db_Adapterの機能を関数ベースで実装したものなんだろうって予想は間違いだった。PDOは関数ベースではなく、PHPの組み込みクラスとして実装されていた)。まあでもこの辺は深追いしても大して面白くなさそうだから追わない。

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その11 Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments
243月/06

Zend Frameworkをどう使うか その10だと思う

さて、Zend_Db周りを見てみよう。「確かO/Rマッパーとして、Zend_Db_DataObjectが作られると発表されていたはずだけど、0.1.2にはそんなの入ってないなー。まだできてないのかなー」と思いつつマニュアルを読んだら、Zend_Db_Table、Zend_Db_Table_Row、Zend_Db_Table_RowsetあたりがO/Rマッパーな機能を持っているのね。名前が変わったのか。

で、Zend_DbのコアであるZend_Db_Adapterが、DB操作全般の基本機能をすべて持っている、PEAR DB相当のもの。PEAR DBと比べるとメソッド名とかずいぶんきれいに整理されつつ、便利そうな類似メソッドもいろいろ増えていて、非常に良さそう(ちなみに俺はMDBとかMDB2とかは使ってないんで、そっちとは比べられない)。

と思いつつよく見たら、Zend_Db_Adapterの設計(インターフェース)って、PDOの機能を元にしているのね。PDOが使える場合はZend_Db_AdapterはPDOのラッパー的に動作し、PDO以外のDBドライバを使う場合は、Zend_Db_Adapter側(の各ドライバエンジン。Zend_Db_Adapter_*とかZend_Db_Statement_*とか)でPDO相当の機能を実装して互換性を保とうってアプローチか。というわけで、Zend_Db周りを理解するためには、PDOに関する基礎知識が必要そう。まあでも面倒くさいからそっちのお勉強は省略。多分Zend_Db_Adapterの機能を関数ベースで実装したものなんだろう。

ところでZend_DB::factory(≒PDO)って、PEAR DBな頃のようなDSN文字列は受け取ってもらえず、配列形式のDBコネクション設定を渡さなきゃいけないみたいだね。たとえばマニュアルには、

$params = array ('host'     => '127.0.0.1',
'username' => 'malory',
'password' => '******',
'dbname'   => 'camelot');
$db = Zend_Db::factory('pdo_mysql', $params);

なんて例が書かれているけど、これって、

$dsn = 'pdo_mysql://malory:****@127.0.0.1/camelot';
$db = Zend_DB::factory($dsn);

を受けつけてくれても良さそうな気がするんだけど、なんでそうなってないんだろう? 従来のURI形式の文字列だと表現しきれないような細かい設定に対応するためかなー。でも過去(PEAR)互換性はキープしてくれてもいいと思うんだけどなー。DBコネクション設定なんて設定ファイルに一行で書いておいてDBライブラリにそれを渡すだけ、って感じにしたいのに。

そういえばZend_Db_Adapter(≒PDO)では、エスケープ関連の機能が充実していて、単純エスケープメソッドだけでなく、

$where = $db->quoteInto('foo = ? and bar = ?', 1, 'BAR'); // foo = 1 and bar = 'BAR'

みたいにprepareもどきな書き方ができる。あと、

$result = $db->query(
'select * from tbl where foo = :foo and bar = :bar',
array('foo' => 1, 'bar' => 'BAR')
); // select * from tbl where foo = 1 and bar = 'BAR'

みたいにprepareを使うこともできる模様。この辺の記法が充実していると、SQL文を書くときにいろいろきれいに書けるようになって便利。O/Rマッパーを使ったところで、いざとなったらSQL文(whereだけとかでも)は書かなきゃいけなくなるわけだし、そういう時の利便性を考えていろいろ用意しているっぽい。

って書いているうちにずいぶん長くなってきたからいったん休憩。

このエントリーを含むはてなブックマークはてなブックマーク - Zend Frameworkをどう使うか その10だと思う Share on Tumblr このエントリをつぶやくこのWebページのtweets
Filed under: 日記 No Comments