大量のデータをあつかう処理のために、Doctrine2にはBatch Processingがあります。このBatch Processingは、findBy()
とは違って結果をメモリに溜め込まみません。foreach
のループ1回ごとに結果を逐次生成するため、メモリにやさしいのです。CSV出力など全データを参照するような場合は、このBatch Processingを使うほういいです。データが増えた時にもメモリ不足を回避できるからです。
しかし、Batch Processingは自分でメモリ管理をする必要があります。Doctrineは一意マッピング(Identity Map)をしているため、一度生成されたEntityはDoctrine側で参照を持っています。そのため、下記の例の(1)のように、ループごとに EntityManager::detach()
をコールして Entity を開放しなければなりません。これでは、Doctrineについてよく知らない開発者が detach() を忘れてしまい、結局メモリ不足に陥ってしまうかもしれません。
また、Doctrineが返すイテレータはなぜか Entity を含む配列になっていて、下記の例の(2)のように、Entityを配列から取り出す処理を書かなければなりません。これでは一見何をしているか分かりにくいですね。あまり直感的ではありません。
<?php
$query = $entityManager->createQuery('SELECT p FROM AcmeBlogBundle:Post p');
$iterableResult = $query->iterate();
foreach ($iterableResult AS $row) {
var_dump($row[0]->getTitle()); // ・・・ (2)
$entityManager->detach($row[0]); // ・・・ (1)
}
- ループごとに
detach()
しないといけない - イテレータの戻り値が直感的でない
そこで、CallbackFilterIterator
を使って上記の2つの問題をカプセル化する方法をご紹介します。 CallbackFilterIterator
は PHP5.4 から追加されたクラスです。各要素について、コールバック関数を実行することができます。以下が、CallbackFilterIterator
を使ったBatch Processingの実装です。
<?php
$query = $entityManager->createQuery('SELECT p FROM AcmeBlogBundle:Post p');
$posts = $query->iterate();
$posts = new CallbackFilterIterator($posts, function(&$current, $key, $iterator) use($entityManager) {
$current = $current[0]; // $current から Entity オブジェクトを取り出してセットしてあげる
$entityManager->detach($current); // イテレータの中でデタッチしてあげる
return true;
});
foreach ( $posts as $post ) {
var_dump($post->getTitle());
}
このままだと、コントローラがごちゃごちゃしてしまうので、CallbackFilterIterator部分はRepositoryでカプセル化しておくといいですね。
<?php
class PostRepository
{
/**
* すべてのブログ投稿を返す
* @return Post[]
*/
public function getWholePostIterator()
{
$query = $entityManager->createQuery('SELECT p FROM AcmeBlogBundle:Post p');
$posts = $query->iterate();
$posts = new CallbackFilterIterator($posts, function(&$current, $key, $iterator) use($entityManager) {
$current = $current[0];
$entityManager->detach($current);
return true;
});
return $posts;
}
}
class CSVExportController
{
public function exportAction()
{
$postRepository = . . .
$posts = $postRepository->getWholePostIterator();
foreach ( $posts as $post ) {
// CSV 吐き出し処理
}
}
}