Skip to content

批量处理

free2one edited this page Jul 13, 2023 · 1 revision

本章向您展示如何使用 Doctrine 高效地完成批量插入、更新和删除。大容量操作的主要问题通常是如何不耗尽内存,本章节所提供的策略将有助于解决此类问题。

ORM 通常不太擅长处理批量插入、更新或删除。每个 RDBMS 都有自己处理此类操作的最有效方法,如果下面列出的选项不足以满足您的目的,我们建议您使用适用于您的特定 RDBMS 的工具来进行这些批量操作。

批量插入

在Doctrine 中进行批量插入最好的方式是分批执行,我们需要尽可能的利用好 EntityManager的事务异步回写机制(transactional write-behind)。下面的代码展示了如何以20为一个批次,并最终完成10000个对象的插入。你可能需要对批量操作的大小进行试验,以便找到最适合实际业务场景的大小。更大的批次数意味着将有更多的预编译语句(prepared statement)可被复用,但也意味着flush期间需要做更多的工作。

$em = EntityManagerFactory::getManager();
$batchSize = 2;
for ($i = 1; $i <= 4; ++$i) {
	$user = new User();
	$user->setUsername('user' . $i);
	$user->setGender(Gender::Male);
	$em->persist($user);
	if (($i % $batchSize) === 0) {
		$em->flush();
		$em->clear();  // 从Doctrine持久化上下文中剔除所有对象
	}
}
$em->flush();  // 持久化剩余对象
$em->clear();

批量更新

使用 Doctrine 进行批量更新有两种方式。

DQL更新

到目前为止,在Doctrine内批量更新最有效的方法是使用 DQL的UPDATE语句。例子:

$em = EntityManagerFactory::getManager();
$q = $em->createQuery('update App\Domain\Entity\User userAlias set userAlias.gender = 2');
$numUpdated = $q->execute();

迭代结果

批量更新的另一种解决方案是使用该Query#toIterable()遍历查询结果,而不是一次将整个结果加载到内存中。例子:

$em = EntityManagerFactory::getManager();  
$batchSize = 20;  
$i = 1;  
$q = $em->createQueryBuilder()->select('userAlias')->from(User::class, 'userAlias');
// $q = $em->createQuery('select userAlias from App\Domain\Entity\User userAlias'); 此语句与上述语句等价

/** @var User $user */  
foreach ($q->getQuery()->toIterable() as $user) {  
	$user->setGender(Gender::Female);  
	if (($i % $batchSize) === 0) {  
		$em->flush(); // 执行所有更新
		$em->clear(); // 从Doctrine持久化上下文中剔除所有对象  
	}  
    ++$i;          
}        
$em->flush();

Doctrine的迭代器使用yield实现,这能有效避免PHP内存不被耗尽,但对于大结果集的查询仍可能存在风险。 “Results may be fully buffered by the database client/ connection allocating additional memory not visible to the PHP process. For large sets this may easily kill the process for no apparent reason.”

另外,你也可以选择使用Hyperf QueryBuilder的chunk进行分块处理(chunk本质为分页查询):

$em = EntityManagerFactory::getManager();  
$builder = $em->createHyperfQueryBuilder();  
$builder  
    ->select('userAlias')  
    ->from($builder->alias(User::class, 'userAlias'))  
    ->orderBy('userAlias.id')  
    ->chunk(20, function ($users) use ($em) {  
        foreach ($users as $user) {  
            $user->setGender(Gender::Female);  
        }        
        $em->flush(); // 执行所有更新
        $em->clear();  
    });

批量删除

与批量更新一样,批量删除最高效的方式仍是使用DQL(DELETE语句)。若选择以迭代方式进行处理,除了需额外调用remove()外,处理策略基本与批量新增一致。

$em = EntityManagerFactory::getManager();  
$batchSize = 20;  
$i = 1;  
$q = $em->createQueryBuilder()->select('userAlias')->from(User::class, 'userAlias');
/** @var User $user */  
foreach ($q->getQuery()->toIterable() as $user) {  
	$em->remove($user);
	if (($i % $batchSize) === 0) {  
		$em->flush(); // 执行所有更新
		$em->clear(); // 从Doctrine持久化上下文中剔除所有对象  
	}  
    ++$i;          
}        
$em->flush();

迭代处理大结果集

在上述流程中我们使用了toIterable()来遍历大结果集,并利用clear()来分离持久化上下文中的所有对象,以保证处理过程中不会出现内存问题。但在一些场景下我们可能希望能够更精准的控制对象的分离行为,此时我们可以选择使用detach

$q = $em->createQueryBuilder()->select('userAlias')->from(User::class, 'userAlias');
foreach ($q->getQuery()->toIterable() as $user) {  
	// 数据处理

	// 从Doctrine持久化上下文中分离对象,随后该对象即可被垃圾回收
	$em->detach($user);
} 
Clone this wiki locally