foreach 循环内部或外部调用 flush() 的区别,使用哪一个?

Difference between call flush() inside foreach loop or outside it, which one to use?

我有这个疑问有一段时间了,但现在是时候问一问了。请参阅下面的代码,在 $someVar 中有大量项目,例如 200 项:

// First approach
foreach($someVar as $item) {
    $item = $em->getRepository('someEntity')->find($item['value']);
    $em->remove($item);
    $em->flush();
}

// Second approach
foreach($someVar as $item) {
    $item = $em->getRepository('someEntity')->find($item['value']);
    $em->remove($item);
}

$em->flush();

真实案例测试

在答案中提供了很好的信息后,我仍有一些疑问。看看这段代码:

foreach ($request->request->get( 'items' ) as $item) {
    $items = $em->getRepository( 'AppBundle:FacturaProductoSolicitud' )->find( $item['value'] );

    try {
        $em->remove( $items );
        $em->flush();

        array_push($itemsRemoved, $item['value']);
        $response['itemsRemoved'] = $itemsRemoved;
        $response['success'] = true;
    } catch ( \Exception $e ) {
        dump( $e->getMessage() );

        array_push($itemsNonRemoved, $item['value']);
        $response['itemsNonRemoved'] = $itemsNonRemoved;
        $response['success'] = false;
    }
}

我在这里使用了一个 try {} catch() {} 句子,因为我需要知道哪个 $item['value'] 被删除或没有被删除,以便将它添加到正确的数组中(参见 $itemsRemoved$itemsNonRemoved) 也为每个循环执行 flush(),我知道这是不好的做法,但是,从 foreach 循环中取出 flush 并在 try-catch 中执行,有没有办法得到哪个 $item['value'] 被删除了?怎么样?

实际上,运行 每次删除后的 flush() 是一个反模式。理想情况下,您应该在所有查询结束时 运行 它一次。

在大多数情况下,Doctrine 2 已经为您处理了正确的事务划分:所有写操作(INSERT/UPDATE/DELETE)都排队等待 EntityManager#flush() 被调用,它将所有这些更改包装在单笔交易。

但是,如果您想要更高的一致性,可以将查询包装在事务中。事实上,它受到 Doctrine 的鼓励,您可以在其 best practices.

中阅读

两个调用会做同样的事情吗?意思是从数据库中删除记录?

是的,尽管对实体调用删除不会导致立即对数据库发出 SQL DELETE。该实体将在下次调用涉及该实体的 EntityManager#flush() 时被删除。这意味着计划删除的实体仍然可以查询并出现在查询和收集结果中。

因此,循环内的刷新意味着大量 SQL 查询和对数据库的访问,实体将被立即删除。

循环外的刷新意味着 Doctrine 执行一个有效的事务(一次访问您的数据库),但在调用刷新之前实体不会被实际删除。实体只会被标记为已删除。

在性能层面,哪个最好用? (Doctrine 有时会成为记忆杀手)

毫无疑问,在循环外刷新。这也是最佳实践。在某些情况下,您每次 persist/remove/update 实体时确实需要执行刷新,但很少见。

如果这两种方法都很好,我也可以使用相同的 per UPDATE 查询吗?

同样适用于 update/persist。尽量避免刷新内部循环。

最后,请查看 documentation。解释的真好。

如果任何查询由于某种原因失败,我如何捕捉到哪一个?也可能是 Doctrine

给出的错误

您始终可以将 flush 包装在 try/catch 块中,并优雅地捕获查询失败时抛出的异常。

try {
   $em->flush()
}(\Exception $e) {
    // do stuff
    throw $e;// re-thrown Exception
}

当使用隐式事务划分并且在 EntityManager#flush() 期间发生异常时,事务会自动回滚并关闭 EntityManager。

有关该主题的更多信息 here

更新

在您提供的代码中,如果您在循环外使用flush,所有删除操作将属于同一个事务。这意味着如果它们中的任何一个失败,则会抛出异常并发出回滚(所有已删除的操作都将被回滚,因此不会保留在数据库上)。

例如:假设我们有四个 ID 为 1、2、3、4、5、6 的项目,假设删除 4 个失败。

第一个选项->Flush inside loop: 1,2,3被删除。 4 失败抛出异常并结束。

第二个选项->Flush outside loop: 4失败,回滚,none被删除,程序结束。

如果您想要实现的行为是案例 1 中所示的行为,那么一个选项可能就是您正在使用的那个。但是,就性能而言,它确实很昂贵。

然而,有更好的解决方案:例如,您可以使用 preRemove/postRemove 事件的组合来存储在刷新中成功删除的那些实体的 ID(或您想要的任何值)(尽管由于回滚而没有持续存在)。例如,您可以将它们存储在属于 class 的静态数组中(或使用单例或其他)。然后在异常的 catch 子句中,您可以使用该数组对这些项目进行迭代和执行删除操作(当然是在循环外刷新)。

然后您可以 return 数组,以便让用户知道您实际上删除了那些实体,并且是错误的,因为删除过程中出现了问题。