LoginSignup
16
14

More than 5 years have passed since last update.

Doctrine2のBatch ProcessingのdetachをCallbackFilterIteratorでカプセル化してみた

Last updated at Posted at 2012-12-26

大量のデータをあつかう処理のために、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 吐き出し処理
        }
    }
}

16
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14