Ответ 1
Пакетная обработка с доктриной сложнее, чем кажется, даже с помощью iterate()
и IterableResult
.
Так же, как вы ожидали наибольшего преимущества IterableResult
, это то, что он не загружает все элементы в память, а второе преимущество в том, что оно не содержит ссылок на объекты, которые вы загружаете, таким образом IterableResult
doesn ' t предотвратить GC от освобождения памяти от вашего объекта.
Однако есть еще один объект Doctrine EntityManager
(более конкретно UnitOfWork
), который содержит все ссылки на каждый объект, который вы запрашивали явно или неявно (EAGER
ассоциации).
Простыми словами, всякий раз, когда вы получаете какой-либо объект (ы), возвращенный findAll()
findOneBy()
даже через DQL
запросы, а также IterableResult
, тогда ссылка на каждое из этих объектов сохраняется внутри доктрины. Ссылка просто хранится в массиве-помощнике, здесь псевдокод:
$identityMap['Acme\Entities\Image'][0] = $image0;
Итак, поскольку на каждой итерации вашего цикла ваши предыдущие изображения (несмотря на то, что они не присутствуют в области цикла или области IterableResult
), все еще присутствуют внутри этого identityMap
, GC не может их очистить, а потребление памяти - так же, как когда вы звонили findAll()
.
Теперь откройте код и посмотрите, что на самом деле происходит
$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i');
//здесь doctrine только создает объект Query, нет доступа db здесь
$iterable = $query->iterate();
//в отличие от findAll(), при этом вызове нет доступа к db. // Здесь объект Query просто завернут в Iterator
while (($image_row = $iterable->next()) !== false) {
// now upon the first call to next() the DB WILL BE ACCESSED FOR THE FIRST TIME
// the first resulting row will be returned
// row will be hydrated into Image object
// ----> REFERENCE OF OBJECT WILL BE SAVED INSIDE $identityMap <----
// the row will be returned to you via next()
// to access actual Image object, you need to take [0]th element of the array
$image = $image_row[0];
// Do something here!
write_image_data_to_file($image,'myimage.data.bin');
//now as the loop ends, the variables $image (and $image_row) will go out of scope
// and from what we see should be ready for GC
// however because reference to this specific image object is still held
// by the EntityManager (inside of $identityMap), GC will NOT clean it
}
// and by the end of your loop you will consume as much memory
// as you would have by using `findAll()`.
Итак, первое решение - фактически сказать Doctrine EntityManager, чтобы отсоединить объект от $identityMap
. Я также заменил цикл while
на foreach
, чтобы сделать его более читаемым.
foreach($iterable as $image_row){
$image = $image_row[0];
// do something with the image
write_image_data_to_file($image);
$entity_manager->detach($image);
// this line will tell doctrine to remove the _reference_to_the_object_
// from identity map. And thus object will be ready for GC
}
Однако приведенный выше пример имеет несколько недостатков, хотя он представлен в документации по документации для пакетной обработки. Он отлично работает, если ваша организация Image
не выполняет загрузку EAGER
для любых своих ассоциаций. Но если вы EAGERly загружаете какую-либо из ассоциаций, например.
/*
@ORM\Entity
*/
class Image {
/*
@ORM\Column(type="integer")
@ORM\Id
*/
private $id;
/*
@ORM\Column(type="string")
*/
private $imageName;
/*
@ORM\ManyToOne(targetEntity="Acme\Entity\User", fetch="EAGER")
This association will be automatically (EAGERly) loaded by doctrine
every time you query from db Image entity. Whether by findXXX(),DQL or iterate()
*/
private $owner;
// getters/setters left out for clarity
}
Итак, если мы используем ту же часть кода, что и выше, на
foreach($iterable as $image_row){
$image = $image_row[0];
// here becuase of EAGER loading, we already have in memory owner entity
// which can be accessed via $image->getOwner()
// do something with the image
write_image_data_to_file($image);
$entity_manager->detach($image);
// here we detach Image entity, but `$owner` `User` entity is still
// referenced in the doctrine `$identityMap`. Thus we are leaking memory still.
}
Возможным решением может быть использование EntityManager::clear()
вместо или EntityManager::detach()
, который полностью очистит идентификационное отображение.
foreach($iterable as $image_row){
$image = $image_row[0];
// here becuase of EAGER loading, we already have in memory owner entity
// which can be accessed via $image->getOwner()
// do something with the image
write_image_data_to_file($image);
$entity_manager->clear();
// now ``$identityMap` will be cleared of ALL entities it has
// the `Image` the `User` loaded in this loop iteration and as as
// SIDE EFFECT all OTHER Entities which may have been loaded by you
// earlier. Thus you when you start this loop you must NOT rely
// on any entities you have `persist()`ed or `remove()`ed
// all changes since the last `flush()` will be lost.
}
Так надейтесь, что это поможет немного понять итерацию доктрины.