-
Notifications
You must be signed in to change notification settings - Fork 1
批量处理
本章向您展示如何使用 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 进行批量更新有两种方式。
到目前为止,在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);
}