我一直在研究在Doctrine(http://docs.doctrine-project.org/en/2.0.x/reference/batch-processing.html)中使用迭代器进行批处理。我有一个包含20,000张图像的数据库,我想迭代一遍。
我知道使用迭代器应该会阻止Doctrine加载内存中的每一行。但是,两个示例之间的内存使用情况几乎完全相同。我正在计算使用(memory_get_usage() / 1024)
之前和之后的内存使用情况。
$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i');
$iterable = $query->iterate();
while (($image = $iterable->next()) !== false) {
// Do something here!
}
迭代器的内存使用情况。
Memory usage before: 2823.36328125 KB
Memory usage after: 50965.3125 KB
第二个示例使用findAll
方法将整个结果集加载到内存中。
$images = $this->em->getRepository('Acme\Entities\Image')->findAll();
findAll
的内存使用情况。
Memory usage before: 2822.828125 KB
Memory usage after: 51329.03125 KB
答案 0 :(得分:34)
即使在iterate()
和IterableResult
的帮助下,使用学说进行批处理也比看起来更复杂。
正如您所期望的那样IterableResult
的最大好处是它不会将所有元素加载到内存中,第二个好处是它不会保存对您加载的实体的引用,因此{ {1}}不会阻止GC从您的实体中释放内存。
然而,还有另一个对象Doctrine的IterableResult
(更具体地说是EntityManager
),其中包含您明确或隐含查询的每个对象的所有引用(UnitOfWork
协会)。
简单来说,每当您获得EAGER
findAll()
即使通过findOneBy()
查询和DQL
返回的任何实体时,也会引用每个实体实体保存在学说中。引用只是存储在一个assoc数组中,这里是伪代码:
IterableResult
因此,在循环的每次迭代中,您之前的图像(尽管不存在于循环的范围或$identityMap['Acme\Entities\Image'][0] = $image0;
的范围内)仍然存在于此{{1 GC无法清除它们,您的内存消耗与调用IterableResult
时的内存消耗相同。
现在让我们浏览一下代码,看看实际发生了什么
identityMap
//这里doctrine只创建Query对象,这里没有数据库访问
findAll()
//与findAll()不同,在此调用时不会发生数据库访问。 //这里Query对象只包含在Iterator
中$query = $this->em->createQuery('SELECT i FROM Acme\Entities\Image i');
所以第一个解决方案是实际告诉Doctrine EntityManager将对象与$iterable = $query->iterate();
分离。我还将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()`.
循环替换为$identityMap
以使其更具可读性。
while
然而,上面的例子有很少的缺陷,即使它在doctrine's documentation on batch processing中有特色。如果您的实体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
}
加载,则效果很好。但是,如果您正在EAGERly加载任何关联,例如。 :
Image
因此,如果我们使用与上面相同的代码,那么
EAGER
可能的解决方案是使用/*
@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's `$identityMap`. Thus we are leaking memory still.
}
或EntityManager::clear()
,这将完全清除身份地图。
EntityManager::detach()
所以希望这有助于理解教义迭代。
答案 1 :(得分:2)
我坚信使用Doctrine进行批处理或使用MySQL(PDO或mysqli)进行任何迭代都只是一种错觉。
@ dimitri-k提供了一个很好的解释,特别是关于工作单元。问题是导致错过:&#34; $ query-&gt; iterate()&#34;它并没有真正迭代数据源。已经完全获取数据源的只是一个\ Traversable wrapper 。
一个例子表明即使从图片中完全删除Doctrine抽象层,我们仍会遇到内存问题:
echo 'Starting with memory usage: ' . memory_get_usage(true) / 1024 / 1024 . " MB \n";
$pdo = new \PDO("mysql:dbname=DBNAME;host=HOST", "USER", "PW");
$stmt = $pdo->prepare('SELECT * FROM my_big_table LIMIT 100000');
$stmt->execute();
while ($rawCampaign = $stmt->fetch()) {
// echo $rawCampaign['id'] . "\n";
}
echo 'Ending with memory usage: ' . memory_get_usage(true) / 1024 / 1024 . " MB \n";
<强>输出:强>
Starting with memory usage: 6 MB
Ending with memory usage: 109.46875 MB
这里,令人失望的 getIterator()方法:
namespace Doctrine\DBAL\Driver\Mysqli\MysqliStatement
/**
* {@inheritdoc}
*/
public function getIterator()
{
$data = $this->fetchAll();
return new \ArrayIterator($data);
}
您可以使用我的小库来实际使用PHP Doctrine或DQL或纯SQL来传输繁重的表。但是你找到了合适的:https://github.com/EnchanterIO/remote-collection-stream
答案 2 :(得分:0)
如果将原则iterate()
与批处理策略结合使用,则应该能够遍历大型记录。
例如:
$batchSize = 1000;
$numberOfRecordsPerPage = 5000;
$totalRecords = $queryBuilder->select('count(u.id)')
->from('SELECT i FROM Acme\Entities\Image i')
->getQuery()
->getSingleScalarResult(); //Get total records to iterate on
$totalRecordsProcessed = 0;
$processing = true;
while ($processing) {
$query = $entityManager->createQuery('SELECT i FROM Acme\Entities\Image i')
->setMaxResults($numberOfRecordsPerPage) //Maximum records to fetch at a time
->setFirstResult($totalRecordsProcessed);
$iterableResult = $query->iterate();
while (($row = $iterableResult->next()) !== false) {
$image = $row[0];
$image->updateSomethingImportant();
if (($totalProcessed % $batchSize ) === 0) {
$entityManager->flush();
$entityManager->clear();
}
$totalProcessed++;
}
if ($totalProcessed === $totalRecords) {
break;
}
}
$entityManager->flush();
请参见https://samuelabiodun.com/how-to-update-millions-of-records-with-doctrine-orm/
答案 3 :(得分:0)
tl;博士;
运行命令时,使用 --no-debug
或将 Sql 记录器设置为 null 以防止它保存它运行的所有查询。
时不时使用EntityManager::clear()
,内存泄漏几乎为零。
答案 4 :(得分:-1)
结果可能类似,因为db客户端可能正在分配您无法看到的额外内存。您的代码也使用'IterableResult',它返回'$ query-&gt; iterate()';这允许处理大的结果而没有内存问题。只是快速的想法希望它有所帮助。