Jonathan H. Wage 著
Doctrine のビヘイビアを作る
この節では Doctrine 1.2 を用いてビヘイビアを作成する例をご紹介します。 これから作るものは、リレーションシップの件数を簡単に保持しておくもので、毎回クエリを発行しなくても件数を取得できるようにするものです。
この機能はとてもシンプルです。リレーションシップの件数を保持したいと思うものすべてに対し、件数を保持するためのカラムを追加します。
スキーマ
これから使うスキーマです。後ほどビヘイビアのために actAs
という定義を追加していきます。
# config/doctrine/schema.yml Thread: columns: title: type: string(255) notnull: true Post: columns: thread_id: type: integer notnull: true body: type: clob notnull: true relations: Thread: onDelete: CASCADE foreignAlias: Posts
スキーマをビルドするには以下のコマンドを実行します:
$ php symfony doctrine:build --all
テンプレート
まずは Doctrine_Template
の子クラスを作り、件数を保持するカラムをモデルに追加する記述をします。
lib/
ディレクトリ以下に配置すると symfony のオートロードの対象になりますので、そこに置きましょう:
// lib/count_cache/CountCache.class.php class CountCache extends Doctrine_Template { public function setTableDefinition() { } public function setUp() { } }
では Post
モデルに actAs
を定義して CountCache
ビヘイビアを追加します:
# config/doctrine/schema.yml Post: actAs: CountCache: ~ # ...
これで Post
モデルで CountCache
ビヘイビアが使えるようになりましたので、ビヘイビアについて少し説明します。
モデルがインスタンス化されたとき、付属するビヘイビアの setTableDefinition()
と setUp()
メソッドが呼ばれ、マッピング情報が定義されます。これは lib/model/doctrine/base/BasePost.class.php
にある BasePost
クラスなどと同じ挙動です。
これによりプラグアンドプレイ形式でカラム、リレーションシップ、イベントリスナーなどの追加が可能です。
なんとなくしくみがわかったかと思うので、CountCache
ビヘイビアに処理を記述していきます:
class CountCache extends Doctrine_Template { protected $_options = array( 'relations' => array() ); public function setTableDefinition() { foreach ($this->_options['relations'] as $relation => $options) { // Build column name if one is not given if (!isset($options['columnName'])) { $this->_options['relations'][$relation]['columnName'] = 'num_'.Doctrine_Inflector::tableize($relation); } // Add the column to the related model $columnName = $this->_options['relations'][$relation]['columnName']; $relatedTable = $this->_table->getRelation($relation)->getTable(); $this->_options['relations'][$relation]['className'] = $relatedTable->getOption('name'); $relatedTable->setColumn($columnName, 'integer', null, array('default' => 0)); } } }
このコードはリレーションシップの件数を保持するカラムをモデルに追加するものです。
今回は Post
モデルの Thread
へのリレーションシップに対してビヘイビアを追加します。各 Thread
の num_posts
カラムにポストされた件数を保持させましょう。YAML スキーマにビヘイビアのためのオプションを定義します:
# ... Post: actAs: CountCache: relations: Thread: columnName: num_posts foreignAlias: Posts # ...
これで Thread
モデルに現在のポスト件数を保持するための num_posts
カラムができました。
イベントリスナー
次のステップは、レコードが新規作成されたときや削除されたときに常に最新の件数が正しく保持されるよう、イベントリスナーの記述を行います。
class CountCache extends Doctrine_Template { // ... public function setTableDefinition() { // ... $this->addListener(new CountCacheListener($this->_options)); } }
何はともあれ、Doctrine_Record_Listener
を継承した CountCacheListener
クラスを定義しましょう。このクラスは Template クラスから配列形式のオプションを受け取ります。
// lib/model/count_cache/CountCacheListener.class.php class CountCacheListener extends Doctrine_Record_Listener { protected $_options; public function __construct(array $options) { $this->_options = $options; } }
最新の件数を保持するためには以下のイベントを使う必要があります。
postInsert(): レコードが新規作成されたときに件数を増やします。
postDelete(): レコードが削除されたときに件数を減らします。
preDqlDelete(): DQL 経由で削除が実行されたときに件数を減らします。
まずは postInsert()
メソッドの定義を行います。
class CountCacheListener extends Doctrine_Record_Listener { // ... public function postInsert(Doctrine_Event $event) { $invoker = $event->getInvoker(); foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']); $table ->createQuery() ->update() ->set($options['columnName'], $options['columnName'].' + 1') ->where($relation['local'].' = ?', $invoker->$relation['foreign']) ->execute(); } } }
上記のコードは、設定したリレーションシップに該当するレコードが下記のようにして新規作成されたときに、DQL の UPDATE 文を用いて件数を増やすものです:
$post = new Post(); $post->thread_id = 1; $post->body = 'body of the post'; $post->save();
この場合は id
が 1
である Thread
の num_posts
カラムが 1
増加します。
これでレコードが作成されたときに件数が増えるようになりました。次にレコードが削除されたときに件数を減らすために postDelete()
メソッドを実装します:
class CountCacheListener extends Doctrine_Record_Listener { // ... public function postDelete(Doctrine_Event $event) { $invoker = $event->getInvoker(); foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']); $table ->createQuery() ->update() ->set($options['columnName'], $options['columnName'].' - 1') ->where($relation['local'].' = ?', $invoker->$relation['foreign']) ->execute(); } } }
この postDelete()
メソッドは num_posts
の値を 1
減らす以外、先ほどの postInsert()
メソッドとほぼ同じものです。下記は先ほど作成した $post
レコードを削除するコードで、このような場合に上記のコードが実行されます:
$post->delete();
パズルの最後のピースとなるのは DQL を用いてレコードを削除した場合の処理です。この場合は preDqlDelete()
メソッドで対処できます:
class CountCacheListener extends Doctrine_Record_Listener { // ... public function preDqlDelete(Doctrine_Event $event) { foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']); $q = clone $event->getQuery(); $q->select($relation['foreign']); $ids = $q->execute(array(), Doctrine::HYDRATE_NONE); foreach ($ids as $id) { $id = $id[0]; $table ->createQuery() ->update() ->set($options['columnName'], $options['columnName'].' - 1') ->where($relation['local'].' = ?', $id) ->execute(); } } } }
このコードは発行された DQL
の DELETE
文を複製して SELECT
文に変換し、削除しようとしているレコードの ID
を取得してきて、対応するレコードのリレーションシップ件数を減らしています。
下記のような場合にリレーションシップ件数が自動的に減るようになります:
Doctrine::getTable('Post') ->createQuery() ->delete() ->where('id = ?', 1) ->execute();
また、下記のように複数のレコードを削除することもできますが、その場合でもリレーションシップ件数は減ります:
Doctrine::getTable('Post') ->createQuery() ->delete() ->where('body LIKE ?', '%cool%') ->execute();
note
preDqlDelete()
などの DQL 発行時に実行されるメソッドは、パフォーマンスを考慮してデフォルトで無効に設定されています。これらのメソッドを有効にするためには以下のように設定の変更を行う必要があります。
$manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);
これでビヘイビアの実装が完了しましたので、最後に少しテストをしましょう。
テスト
先ほど実装したコードのテストのためにフィクスチャを作りましょう:
# data/fixtures/data.yml Thread: thread1: title: Test Thread Posts: post1: body: This is the body of my test thread post2: body: This is really cool post3: body: Ya it is pretty cool
ではモデルとデータを再構築して、フィクスチャを読み込ませましょう:
$ php symfony doctrine:build --all --and-load
これで再構築とフィクスチャの読み込みが完了しました。まずはリレーションシップの件数が正しく保持されているか見てみましょう:
$ php symfony doctrine:dql "FROM Thread t, t.Posts p" doctrine - executing: "FROM Thread t, t.Posts p" () doctrine - id: '1' doctrine - title: 'Test Thread' doctrine - num_posts: '3' doctrine - Posts: doctrine - - doctrine - id: '1' doctrine - thread_id: '1' doctrine - body: 'This is the body of my test thread' doctrine - - doctrine - id: '2' doctrine - thread_id: '1' doctrine - body: 'This is really cool' doctrine - - doctrine - id: '3' doctrine - thread_id: '1' doctrine - body: 'Ya it is pretty cool'
Thread
モデルの num_posts
が 3
になっているのが確認できると思います。次にポストを1件削除して件数が減っているかテストしましょう:
$post = Doctrine_Core::getTable('Post')->find(1); $post->delete();
レコードが削除され、件数が更新されているのが確認できると思います:
$ php symfony doctrine:dql "FROM Thread t, t.Posts p" doctrine - executing: "FROM Thread t, t.Posts p" () doctrine - id: '1' doctrine - title: 'Test Thread' doctrine - num_posts: '2' doctrine - Posts: doctrine - - doctrine - id: '2' doctrine - thread_id: '1' doctrine - body: 'This is really cool' doctrine - - doctrine - id: '3' doctrine - thread_id: '1' doctrine - body: 'Ya it is pretty cool'
DQL の DELETE 文で残りの2レコードをまとめて削除しても動作するかテストしましょう:
Doctrine_Core::getTable('Post') ->createQuery() ->delete() ->where('body LIKE ?', '%cool%') ->execute();
関連するすべてのポストを削除したので、num_posts
は 0 になっていなければなりません:
$ php symfony doctrine:dql "FROM Thread t, t.Posts p" doctrine - executing: "FROM Thread t, t.Posts p" () doctrine - id: '1' doctrine - title: 'Test Thread' doctrine - num_posts: '0' doctrine - Posts: { }
バッチリです!ここで学んだビヘイビアについてのことや、ビヘイビアそのものがお役に立てることを願っています!
結果キャッシュを使う
トラフィックの多い Web アプリケーションではキャッシュを使ってリソースを節約する必要があります。最新の Doctrine 1.2 では結果セットキャッシングに対する数々の改良がされており、キャッシュドライバからキャッシュの削除が可能になりました。これまでは削除の際にキャッシュのキーを特定することができず、削除することができませんでした。
この節では全ユーザーを取得するクエリの結果セットをキャッシュする方法と、データに変更が行われた際にイベントを用いてキャッシュをクリアする簡単な例をご紹介します。
スキーマ
今回は以下のスキーマを使います:
# config/doctrine/schema.yml User: columns: username: type: string(255) notnull: true unique: true password: type: string(255) notnull: true
スキーマをビルドするには以下のコマンドを実行します:
$ php symfony doctrine:build --all
これで下記のような User
クラスが作られます:
// lib/model/doctrine/User.class.php /** * User * * This class has been auto-generated by the Doctrine ORM Framework * * @package ##PACKAGE## * @subpackage ##SUBPACKAGE## * @author ##NAME## <##EMAIL##> * @version SVN: $Id: Builder.php 6508 2009-10-14 06:28:49Z jwage $ */ class User extends BaseUser { }
のちほどこのクラスにコードを追加しますので覚えておいてください。
結果キャッシュの設定
結果キャッシュを使うためにはまずキャッシュドライバに問い合わせるための設定が必要です。これは ATTR_RESULT_CACHE
属性の設定で行えます。今回は運用運用時にもっとも適しているであろう APC キャッシュドライバを使います。もし APC が使えない場合は Doctrine_Cache_Db
や Doctrine_Cache_Array
がありますので試してみてください。
属性の設定は ProjectConfiguration
で行います。configureDoctrine()
を定義しましょう:
// config/ProjectConfiguration.class.php // ... class ProjectConfiguration extends sfProjectConfiguration { // ... public function configureDoctrine(Doctrine_Manager $manager) { $manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE, new Doctrine_Cache_Apc()); } }
キャッシュドライバの設定ができたので、実際にクエリの結果セットをキャッシュしていきましょう。
サンプルクエリ
多数のユーザーがクエリに関連してくるアプリケーションがあったとして、ユーザーの情報に変更があった場合は毎回キャッシュの情報をクリアしたいとしましょう。
ここにアルファベット順にソートされたユーザーの一覧を表示するためのものであろうクエリがあります:
$q = Doctrine_Core::getTable('User') ->createQuery('u') ->orderBy('u.username ASC');
このクエリをキャッシュするには useResultCache()
メソッドを使います:
$q->useResultCache(true, 3600, 'users_index');
note
第3引数はキャッシュドライバにキャッシュを保持させるためのキーになります。これによりキャッシュドライバへの問い合わせや削除を簡単に行えるようになります。
クエリを実行してデータベースに問い合わせを行い、その結果を users_index
というキーでキャッシュドライバに保持させ、それ以降の結果はデータベースの代わりにキャッシュドライバが返します:
$users = $q->execute();
note
キャッシュにより Doctrine は既にハイドレーションされた情報を保持しているので、ハイドレーション処理は飛ばされます。これによりデータベースサーバーはもちろん、Web サーバーへの負荷も抑えられます。
キャッシュドライバにキャッシュ情報があるかどうかは、users_index
キーを用いてチェックできます:
if ($cacheDriver->contains('users_index')) { echo 'cache exists'; } else { echo 'cache does not exist'; }
キャッシュの削除
クエリはキャッシュされたので、削除について学ぶ必要があります。削除をするにはキャッシュドライバの API を手動でたたくほか、ユーザー情報が保存・更新された際にイベントを用いて自動的にキャッシュを削除するやり方があります。
キャッシュドライバ API
まずはキャッシュドライバの API を使う例をご説明します。
tip
結果キャッシュドライバのインスタンスは Doctrine_Manager
クラスのインスタンスから取得できます。
$cacheDriver = $manager->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
もし $manager
にインスタンスが入っていない場合は以下のようにして取得できます。
$manager = Doctrine_Manager::getInstance();
では API を使って先ほどのキャッシュを削除してみましょう:
$cacheDriver->delete('users_index');
他に users_
ではじまるキャッシュがある場合にそれらもまとめて削除したいと思いますが、これは delete()
メソッド単独ではまとめて行えません。まとめて削除したい場合は deleteByPrefix()
メソッドに接頭辞を渡すことで対応できます:
$cacheDriver->deleteByPrefix('users_');
他にもキャッシュを削除するための便利なメソッドが用意されています:
deleteBySuffix($suffix)
: 接尾辞を渡してキャッシュを削除します。deleteByRegex($regex)
: 正規表現にマッチするキャッシュを削除します。deleteAll()
: すべてのキャッシュを削除します。
イベントを用いて削除
ユーザー情報に変更があった場合には自動的にキャッシュがクリアされるのが望ましいでしょう。User
モデルに postSave()
イベントメソッドを実装して対応しましょう。
以前に User
クラスについて話したのを思い出しましたか?ではクラスをお好きなエディタで開いて、以下のように postSave()
メソッドを追加しましょう:
// lib/model/doctrine/User.class.php class User extends BaseUser { // ... public function postSave($event) { $cacheDriver = $this->getTable()->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE); $cacheDriver->deleteByPrefix('users_'); } }
これでユーザーの更新や新規作成が行われたときにユーザー情報に関連するクエリのキャッシュが削除されるようになりました。
$user = new User(); $user->username = 'jwage'; $user->password = 'changeme'; $user->save();
次回クエリが発行されたときにはキャッシュは存在せず、新しい情報をデータベースから取得して再度キャッシュを行い、また以降そのキャッシュを使うようになります。
Doctrine のハイドレータを作る
Doctrine の鍵となる機能のひとつに Doctrine_Query
オブジェクトをさまざまな結果セット構造に変換する機能があります。これを行うのが Doctrine のハイドレータですが、Doctrine 1.2 まではハイドレータはハードコーディングされていて、自前のハイドレータは作れませんでした。現在はそれが可能になっており、Doctrine_Query
でデータベースから取得してきた情報を好きな構造にすることができます。
ここではとてもシンプルでわかりやすく、とっても使えるハイドレータを作っていきます。今回作るものは 2 つのカラムを取得してきて、1 つめのカラムを配列のキーに、2 つめのカラムを配列の値にして返すハイドレータを作りましょう。
スキーマとフィクスチャ
まずはスキーマを作ります。今回はシンプルな User
モデルを使います:
# config/doctrine/schema.yml User: columns: username: string(255) is_active: string(255)
また、テストのためにフィクスチャを用意したので、以下からコピーしましょう:
# data/fixtures/data.yml User: user1: username: jwage password: changeme is_active: 1 user2: username: jonwage password: changeme is_active: 0
これをビルドするには以下のコマンドを実行します:
$ php symfony doctrine:build --all --and-load
ハイドレータを作る
ハイドレータは Doctrine_Hydrator_Abstract
を継承したクラスに hydrateResultSet($stmt)
メソッドを実装するだけで作れます。このメソッドはクエリオブジェクトによって作られた PDOStatement
インスタンスを受け取ります。このステートメントに対してクエリを発行し、返ってきた生の値を好きな構造に変換できます。
このステートメントからクエリを発行して生の値を取得し、それを好きな構造に変換した後に
では KeyValuePairHydrator
というクラスを作って、symfony のオートロードの対象となる lib/
ディレクトリ以下に配置しましょう:
// lib/KeyValuePairHydrator.class.php class KeyValuePairHydrator extends Doctrine_Hydrator_Abstract { public function hydrateResultSet($stmt) { return $stmt->fetchAll(Doctrine_Core::FETCH_NUM); } }
上記のコードは PDO から取得した情報をそのまま返すもので、欲しい結果とは全く異なります。欲しいのは キー => 値 形式に変換された情報です。ではそれにあわせて hydrateResultSet()
メソッドを変更していきましょう:
// lib/KeyValuePairHydrator.class.php class KeyValuePairHydrator extends Doctrine_Hydrator_Abstract { public function hydrateResultSet($stmt) { $results = $stmt->fetchAll(Doctrine_Core::FETCH_NUM); $array = array(); foreach ($results as $result) { $array[$result[0]] = $result[1]; } return $array; } }
とても簡単ですね!このハイドレータのコードはこれで完成なので、テストをしていきましょう!
ハイドレータを使う
このハイドレータを使ってクエリを発行しテストをしていくためには、まず Doctrine にハイドレータを登録する必要があります。
登録は ProjectConfiguration
の内部で Doctrine_Manager
のインスタンスに対して行います:
// config/ProjectConfiguration.class.php // ... class ProjectConfiguration extends sfProjectConfiguration { // ... public function configureDoctrine(Doctrine_Manager $manager) { $manager->registerHydrator('key_value_pair', 'KeyValuePairHydrator'); } }
これでハイドレータが登録できたので、Doctrine_Query
インスタンスに対してこれを使うようにしてみましょう:
$q = Doctrine_Core::getTable('User') ->createQuery('u') ->select('u.username, u.is_active'); $results = $q->execute(array(), 'key_value_pair'); print_r($results);
先ほど定義したフィクスチャを使って上記のクエリを実行した場合の結果は以下のようになります:
Array ( [jwage] => 1 [jonwage] => 0 )
できました!とってもシンプルでしょう?この内容があなたのお役に立てることを願っていますし、このおかげでコミュニティにも新たにいくつかのすばらしいハイドレータが寄付されました。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.