モノノフ日記

普通の日記です

Propel 1.4のWhatsNewの超訳

この前のsymfony1.4勉強会では、doctrineばっかり言及されててPropelはあんまり触れられてませんでした。1.0から使ってる人はPropelに慣れきってると思うし、あと1.4の情報知らない人が多いのかな、と思って新要素のページを超訳してみたので公開しておきます。*1

翻訳元のページは http://propel.phpdb.org/trac/wiki/Users/Documentation/1.4/WhatsNew です。

What's new in Propel 1.4?

Propel1.4は1.3と後方互換性を保ったままのアップグレードになります。多くのバグフィックス、面白い新要素、速度改善などがあります。アップグレードした後はモデルの再ビルドを忘れないようにしようね。

save()メソッド、delete()メソッドでPre, Postフックをサポート

オブジェクトとして生成された save() と delete() メソッドは簡単にオーバーライドできるよ。実際にはPropelは必要となったときに下に書いてるメソッドの中から探してくるよ。

<?php
preInsert()            // code executed before insertion of a new object
postInsert()           // code executed after insertion of a new object
preUpdate()            // code executed before update of an existing object
postUpdate()           // code executed after update of an existing object
preSave()              // code executed before saving an object (new or existing)
postSave()             // code executed after saving an object (new or existing)
preDelete()            // code executed before deleting an object
postDelete()           // code executed after deleting an object

例えば、下の例だとINSERTする前に created_at カラムを必ず更新してくれるよ。

<?php
class Book extends BaseBook
{
  public function preInsert(PropelPDO $con = null)
  {
    $this->setCreatedAt(time());
  }
}

この新しい要素は書き込み操作にちょっとオーバーヘッドが出るから、必要ないと思うなら設定ファイルからfalseにしてね。

# -------------------
#  TEMPLATE VARIABLES
# -------------------
propel.addHooks = false
ビヘイビア

モデル間のビヘイビアをまとめたり、再利用できるようになったよ。
例えばオブジェクトが生成された日時や最新の更新日時をキープするにはシンプルにテーブル定義に timestampable ビヘイビアを定義するだけでできるよ。

<table name="book">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="title" type="VARCHAR" required="true" />
  <behavior name="timestampable" />
</table>

モデルを再ビルドして、bookテーブルが自動で生成日時と更新日時をセットする2つの新しいカラムを持ってることを確認しよう。

<?php
$b = new Book();
$b->setTitle('War And Peace');
$b->save();
echo $b->getCreatedAt(); // 2009-10-02 18:14:23
echo $b->getUpdatedAt(); // 2009-10-02 18:14:23
$b->setTitle('Anna Karenina');
$b->save();
echo $b->getCreatedAt(); // 2009-10-02 18:14:23
echo $b->getUpdatedAt(); // 2009-10-02 18:14:25

Propelビヘイビアはビルド時間が発生するけど、オーバーヘッドはほとんど無いと思っていいよ。スキーマファイルを編集すれば、Propelがメソッドを変更してモデルとPeerクラスの両方にメソッドを追加してくれるよ。HowTos セクションも読んでね。

Propel 1.4はtimestampable や soft_deleteといった、いくつかのビヘイビアをバンドルしてるよ。詳しくはドキュメントを読んでね。

マルチプル条件のJOIN

addMultipleJoin() メソッドを使うといくらでもJOINできちゃうよ。

<?php
$c = new Criteria();
$c->addMultipleJoin(array(
    array(ReaderFavoritePeer::BOOK_ID, BookOpinionPeer::BOOK_ID),
    array(ReaderFavoritePeer::READER_ID, BookOpinionPeer::READER_ID))
  Criteria::INNER_JOIN);
// SQL result
SELECT ...
FROM reader_favorite 
INNER JOIN book_opinion ON (reader_favorite.BOOK_ID = book_opinion.BOOK_ID 
                        AND reader_favorite.READER_ID = book_opinion.READER_ID)

複雑なJOINができるように各JOINの条件ごとに第3引数を追加できるよ。

<?php
$c = new Criteria();
$c->addMultipleJoin(array(
    array(Book::USER_ID, UserPeer::ID))
    array(UserPeer::RANK, 12, Criteria::GREATER_THAN),
  Criteria::LEFT_JOIN);
// SQL result
SELECT ...
FROM book
LEFT JOIN user ON (book.USER_ID = user.ID AND user.RANK > 12)

複雑なJOINするのによく知られた方法だった、addJoin() メソッドの引数に配列を使うのは非推奨になったので注意しようね。

フルクエリロギング

Propelはバインドされたパラメータ付きでデータベースへの全てのクエリをロギングしてるよ。だから、Propelのログから簡単にコピーしたり、結果を見ながらデータベースへ実行したりできるよ。

それに加えて、各クエリごとにかかった時間や消費したメモリを記録するから、slowクエリだけや設定した閾値を超えたクエリだけロギングできるよ。

CreoleからPDOにスイッチした影響で、Propel1.3ではこのパワフルなロギングは無くなっちゃってたんだ。1.4からは1.2みたいなロギングができるようになってるよ。

詳しくはフルクエリロギングのドキュメントをチェックしてね。

Baseオブジェクトに__toString()

スキーマファイルでprimaryStringをカラムに定義しておくと、Propelがモデルオブジェクトに __toString() メソッドを作ってくれるよ。

<table name="book" phpName="Book">
  ..
  <column name="title" type="varchar" size="125" primaryString="true" />
  ..
</table>

モデルをビルドした後、作られた BaseBook クラスは下記のマジックメソッドを提供してくれるよ。

<?php
public function __toString()
{
  return (string) $this->getTitle();
}

これはBookオブジェクトが返す文字列が titleカラムの値になる、ということを意味してるよ。

<?php
$book = new book();
$book->setTitle('War And Peace');
echo $book; // 'War And Peace'
新しいPeer定数: OM_CLASS

Peerクラスに関連するモデルクラスを取得したい、と思ったことはない? そうしたいときは、パッケージの情報が一緒に含まれちゃってる定数を使うしかなかったんじゃないかなと思うよ。

<?php
// in BaseBookPeer.php
/** A class that can be returned by this peer. */
const CLASS_DEFAULT = 'bookstore.Book';

この定数は役に立つんだけど、オートロードに使えないんだ。実際に使うときはパッケージの情報を削除する文字列操作が必要になるよね。このめんどくさい処理はPropel 1.4だと必要なくなったよ。Peerクラスの新しい定数を使おうね。

<?php
// in BaseBookPeer.php
/** the related Propel class for this table */
const OM_CLASS = 'Book';

作られたPeerメソッドはこの定数を使えば、文字列操作しなくていいよ。だからちょっとだけ処理が速くなると思うよ。

PropelPagerは Countableとイテレータインターフェースを実装

Propelはペジネート用のクラスを提供してくれてるのは知ってるかな?もっと高性能になって、Countableとイテレータインターフェースをサポートするようになったよ。PropelPagerオブジェクトを配列のように操作できるようになってるよ。

<?php
$c = new Criteria();
$c->add(BookPeer::AUTHOR, $authorId);
$pager = new PropelPager($c, 'BookPeer', 'doSelect', $page = 1, $rowsPerPage = 20);

if(count($pager)) // if the current page has results
{
  foreach($pager as $book) // get pager results and iterate on them
  {
    echo $book->getTitle();
  }
}

PropelPagerの有用性については新しいドキュメントのHowTosセクションに書いてあるからチェックしてみようね。

実行時の Introspection をより良く

いくつかのメソッドはデータベースのリレーションシップで記述される新しい RelationMapクラスであると同時に、Mapクラスに追加され、実行時の introspection を緩和させてるよ。

TableMap    DatabaseMap::getTableByPhpName($name) // TableMap object by object model name, e.g. 'Book'
ColumnMap   DatabaseMap::getColumn($name) // ColumnMap object by fully qualified name, e.g. book.AUTHOR_ID
Array       TableMap::getPrimaryKeys()    // List of the ColumnMap objects corresponding to the table primary keys
Array       TableMap::getForeignKeys()    // List of the ColumnMap objects corresponding to the table foreign keys
Array       TableMap::getRelations()      // List of the table relationships, as RelationMap objects
String      TableMap::getPackage()        // Package of the table
mixed       ColumnMap::getDefaultValue()  // Default value defined in the schema for this column
TableMap    ColumnMap::getRelatedTable()  // Related TableMap object by foreign key
ColumnMap   ColumnMap::getRelatedColumn() // Related ColumnMap object by foreign key
RelationMap ColumnMap::getRelation()      // Related RelationMap object   
integer     RelationMap::getType()        // RelationMap::ONE_TO_MANY, RelationMap::MANY_TO_ONE, or RelationMap::ONE_TO_ONE
string      RelationMap::getOnDelete()    // ON DELETE directive, e.g. 'SET NULL'
TableMap    RelationMap::getLocalTable()  // Local TableMap object (the one bearing the fkey)
TabkeMap    RelationMap::getForeignTable()   // Foreign TableMap object
Array       RelationMap::getColumnMappings() // List of local => foreign column for this relation, e.g array('book.PUBLISHER_ID' => 'publisher.ID')
Array       RelationMap::getLocalColumns()   // List of the ColumnMap objects on the local side of the relation
Array       RelationMap::getForeignColumns() // List of the ColumnMap objects on the foreign side of the relation

HowTos セクションにある新しい Runtime Introspection のドキュメントをチェックしてみてね。
あと、PeerクラスからPropelオブジェクトクラスの名前を簡単にゲットできるよ。

echo BookPeer::getOMClass()                    => 'bookstore.Book'
echo BookPeer::getOMClass($withPrefix = false) => 'Book'
MapBuilders はなくなりました

Runtime introspection は以前は MapBuildersと呼ばれるTablemapオブジェクトを runtimeのビルダークラスとして依存してたよ。MapBuildersクラスはもう作られなくなってるんだ。代わりに、簡単なTableMapクラスを使ってるよ。

TableMapをゲットする普通の方法は以前と同じようにPeerクラスを通して行うよ。

<?php
$bookTableMap = BookPeer::getTableMap();

けど、手動でのTableMapsのビルドはもっと簡単になってるよ。

<?php
// Propel 1.3 way to initialize a TableMap
$className = 'Book';
$mapBuilderClass = $className . 'MapBuilder';
$mapBuilder = new $mapBuilderClass();
if (!$mapBuilder->isBuilt())
{
  $mapBuilder->doBuild();
}
$bookTableMap = $databaseMap->getTable('book');

// Propel 1.4 way to initialize a TableMap
$className = 'Book';
$tableMapClass = $className . 'TableMap';
$bookTableMap = $databaseMap->addTableFromMapClass($tableMapClass);

TableMapsをロードして関連テーブルを引っ張ってくるには、下のように書く必要があることを覚えておこうね。

<?php
// build all the TableMap objects of tables related to Book
$relations = $bookTableMap->getRelations();
新しいビルドオプション

作られるクラスを上手くコントロールできるように、3つの新しい設定がbuild.propertiesに追加されてるよ。

propel.addValidateMethod = {true}|false

クラスにvalidate() メソッドを追加するかどうかを設定するよ。falseにするとPropelバリデーションは使わなくなるよ。

propel.addIncludes = {true}|false

作られるスタブクラスで必要とするステートメントを追加するかどうかを設定するよ。falseにすると、実行時に各クラスがオートロードされるよ。

propel.addHooks = {true}|false

save()メソッドやdelete()メソッドでの pre- や post- フックをサポートするかどうかを設定するよ。falseにするとフックは使えなくなるけど少し処理スピードが上がるよ。

外部キーの取得にはインスタンスプールを利用

外部キーを経由して参照してるモデルから関連するオブジェクトを取得するときは、大抵の場合、下記のようなPropelが作るメソッドを使うよ。

<?php
$author = $book->getAuthor(); // $author is an Author instance

裏では、このメソッドはbookオブジェクトのauthor_idプロパティを使って、データベースにauthorレコードを取得するクエリを作って、返す値としてAuthorオブジェクトをhydrateするよ。 もしスクリプトを実行する以前にauthorオブジェクトがhydrateされてたどうなると思う? インスタンスプールのおかげで、Propelはメモリにオブジェクトを覚えておくことができるよ。たくさんのデータベースクエリを保存することができるんだ。例えば

<?php
foreach($author->getBooks() as $book)
{
  echo $book->getTitle();
  echo $book->getAuthor()->getName();
}

Propel1.3だと、このコードは データベースに n+1 回クエリを投げることになってたよ(n は authorが書いた本の数になるよ)。 だけど、Propel 1.4は1回のクエリで実行できちゃうよ。getAuthor() メソッドが呼ばれると、Propelは参照する本のAuthorをメモリから参照するよ。だから、データベースクエリやhydrateするプロセスは全部スキップしちゃうよ。結果はほんの少しの速度向上になって、1ページで実行されるクエリの数を減らすことになるよ。

BasePeer::populateStmtValues() は publicになります

自作のSQLクエリやBasePeer::createSelectSql() メソッドが返すクエリを編集して使えるよ。クエリへ値をセットするのにはPropelのバインド機能使ってるよ。Propelはバインドすることでカラムの型のPDOを使うんだ。だから、このメソッドは少しイライラさせちゃうかもしれないよ。

<?php
$sql = BasePeer::createSelectSql($criteria, $params);
$sql = "INSERT INTO temp_table_name $sql";
$stmt = $con->prepare($sql);
BasePeer::populateStmtValues($stmt, $params, $dbMap, $db);
$stmt->execute();
ドキュメントの再編成

PropelのドキュメントはSubversionリポジトリに今あるよ。簡単に編集できるし、ドキュメントの変遷をトレースするのも簡単だよ。修正チケットも添付してるし、自分でパッケージングしたPropelライブラリをバンドルしたりできるよ。

もちろん、上で記述されてる全ての変更点については反映したドキュメントがアップデートされてるよ。

2倍のユニットテスト

Propel 1.4の開発プロセスにはTDDを導入したよ。結果として、ユニットテストの数は劇的に増えました。実際、1.3の倍以上の数になってるよ。ユニットテストカバレッジはまだ決して大きい数字じゃないよ。けど、Propelライブラリの安定性と頑健性は毎日よくなっているよ。

*1:ホントに斜め読みなので気になる人は原文チェックしてください。