使用 preRemove/postRemove 事件来获取哪些查询可以执行,哪些不可以

Using preRemove/postRemove events to get which queries can be executed and which can't

这个问题在我脑海中盘旋了一段时间,现在我需要一些关于 preRemove/postRemove 事件的建议,因为我将要执行的查询基本上是 DELETE 但是这个应该也适用于 prePersist/postPersist 和 preUpdate/postUpdate(不知道那些最新的是否真的存在)。

我在几个实体中执行 DELETE 有两种可能的情况(参见 foreach 循环):

// First approach
$itemsRemoved = $itemsNonRemoved = [];
foreach($someVar as $item) {
    $item = $em->getRepository('someEntity')->find($item['value']);
    try {
        $em->remove($item);
        $em->flush();
        array_push($itemsRemoved, $item['value']);
    } catch (Exception $e) {
        dump($e->getMessage());
        array_push($itemsNonRemoved, $item['value']);
    }
}

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

$em->flush();

不推荐第一种方法,正如 @acontell 用户在 回答中所说,执行 flush() 是一种反模式,也会影响应用程序性能因为每次都需要执行多个查询,但使用这种方法我可以得到哪个被插入,哪个没有被插入。

使用第二种方法我将避免反模式并提高性能,但我如何知道插入了哪些项目,哪些没有插入?此外,如果默认情况下任何查询失败,Doctrine 将进行回滚,因此将插入 none。

那么,我可以使用 preRemove/postRemove 事件来获取哪些查询可以执行,哪些不能表示插入或不插入哪些值?

这个问题与有密切关系。

现实生活中的例子

既然 @acontell 给我另一个很好的答案,我需要一些建议,看看我是否掌握了全部内容,或者我仍然迷路了,所以这是一个真实的例子:

        foreach ($request->request->get( 'items' ) as $item) {
            $relacion = $this->get( 'database_connection' )->fetchColumn(
                'SELECT COUNT(fabricante_producto_solicitud_id) AS cnt FROM negocio.fabricante_modelo_marca_producto WHERE fabricante_producto_solicitud_id = ?',
                array( $item['value'] )
            );               

            if ($relacion === 0) {
                $entFabricanteProductoSolicitud = $em->getRepository(
                    "AppBundle:FabricanteProductoSolicitud"
                )->find( $item['value'] );

                try {
                    $em->remove( $entFabricanteProductoSolicitud );
                    $em->flush();
                    array_push( $itemsRemoved, $item['value'] );

                    $response['success'] = true;
                    $status              = 200;
                } catch ( \Exception $e ) {
                    $status = 400;
                    dump( $e->getMessage() );

                    return new JsonResponse( $response, $status ?: 200 );
                }
            }

            $response['itemsRemoved'] = $itemsRemoved;
        }
    }

如果我明白了,那么 LifeCycleCallbacks 应该进入 AppBundle:FabricanteProductoSolicitud 执行 DELETE 的地方,对吗?

编辑: 我还想知道在多个实体上使用代码的最佳方法,因为我在大多数实体中都会有这种行为,那么为此目的定义一个 Trait 应该没问题吧?应该定义为任何其他特征?

此问题已由我自己回答,并使用用户评论作为输入,希望对其他人有所帮助

通过@acontell

对代码进行一些测试

这就是我的代码此时的样子:

public function eliminarNormasAction(Request $request)
{
    if ($request->isXmlHttpRequest()) {
        $em = $this->getDoctrine()->getManager();
        $response['success']  = false;
        $entProducto = $em->getRepository('AppBundle:Producto')->find($request->request->get('producto'));
        $response['success']  = false;
        $status = null;

        $ids = [];
        foreach($request->request->get( 'items' ) as $item) {
            array_push( $ids, $item['value'] );
        }

        $qb = $em->createQueryBuilder();
        $entNorma = $qb
            ->select("q")
            ->from('AppBundle:Norma', 'q')
            ->add('where', $qb->expr()->in('q.id', ':ids'))
            ->setParameter('ids', $ids)
            ->getQuery()
            ->getResult();

        // Initialize arrays (useful to reset them also)
        Entity\Producto::prepareArrays();

        foreach($entNorma as $norma) {
            // here entities are persisted since rows there is not more at DB
            $entProducto->removeProductoNorma( $norma );  
        }

        try {
            $em->flush();
            $response['success'] = true;
        } catch (\Exception $e) {
            $status = 400;
        }

        $response['itemsRemoved']    = Entity\Producto::getDeletedEntities();
        $response['itemsNonRemoved'] = Entity\Producto::getNotDeletedEntities();
    } else {
        $response['error'] = $this->get('translator')->trans('mensajes.msgPeticionXMLHttpRequestInvalida');
    }

    return new JsonResponse($response, $status ?: 200);
}

问题 Entity\Producto::getDeletedEntities() 返回一个没有删除值的空数组,为什么?

这是我的做法。我并不是说这是最好的方法,如果有人知道更简单或更好的东西,我会是第一个有兴趣学习它的人。

首先,这些是 Doctrine events that you can use. For simplicity's sake, I'm going to explain how I'd do it for deletions. Also for simplicity, I'm going to use an static array (it could be done some other ways, I like this one) and lifecycle callbacks. In this case the callbacks are going to be very simple methods (that's why it's ok to use them instead of implementing a listener or subscriber)。

假设我们有这个实体:

Acme\MyBundle\Entity\Car:
    type: entity
    table: cars
    id:
        id:
            type: integer
            id: true
            generator:
                strategy: AUTO
    fields:
        name:
            type: string
            length: '25'
            unique: true
        color:
            type: string
            length: '64'
    lifecycleCallbacks:
        preRemove: [entityDueToDeletion]
        postRemove: [entityDeleted]

如您所见,我定义了两个将由 preRemove 事件和 postRemove 事件触发的回调。

preRemove - The preRemove event occurs for a given entity before the respective EntityManager remove operation for that entity is executed. It is not called for a DQL DELETE statement.

postRemove - The postRemove event occurs for an entity after the entity has been deleted. It will be invoked after the database delete operations. It is not called for a DQL DELETE statement.

然后实体的php代码:

class Car {

    // Getters & setters and so on, not going to copy them here for simplicity

    private static $preDeletedEntities;// static array that will contain entities due to deletion.
    private static $deletedEntities;// static array that will contain entities that were deleted (well, at least the SQL was thrown).

    public function entityDueToDeletion() {// This callback will be called on the preRemove event
        self::$preDeletedEntities[] = $this->getId();// This entity is due to be deleted though not deleted yet.
    }

    public function entityDeleted() {// This callback will be called in the postRemove event
        self::$deletedEntities[] = $this->getId();// The SQL to delete the entity has been issued. Could fail and trigger the rollback in which case the id doesn't get stored in the array.
    }

    public static function getDeletedEntities() {
        return array_slice(self::$preDeletedEntities, 0, count(self::$deletedEntities));
    }

    public static function getNotDeletedEntities() {
        return array_slice(self::$preDeletedEntities, count(self::$deletedEntities)+1, count(self::$preDeletedEntities));
    }

    public static function getFailedToDeleteEntity() {
        if(count(self::$preDeletedEntities) == count(self::$deletedEntities)) {
            return NULL; // Everything went ok
        }
        return self::$preDeletedEntities[count(self::$deletedEntities)]; // We return the id of the entity that failed.
    }

    public static function prepareArrays() {
        self::$preDeletedEntities = array();
        self::$deletedEntities = array();
    }
}

注意回调以及静态数组和方法。每次对 Car 实体调用 remove 时,preRemove 回调会将实体的 ID 存储在数组 $preDeletedEntities 中。当删除实体时,postRemove 事件会将 id 存储在 $entityDeleted 中。 preRemove 事件很重要,因为我们想知道是哪个实体导致交易失败。

现在,在控制器中我们可以这样做:

use Acme\MyBundle\Entity\Car;

$qb = $em->createQueryBuilder();
$ret = $qb
        ->select("c")
        ->from('AcmeMyBundle:Car', 'c')
        ->add('where', $qb->expr()->in('c.id', ':ids'))
        ->setParameter('ids', $arrayOfIds)
        ->getQuery()
        ->getResult();

Car::prepareArrays();// Initialize arrays (useful to reset them also)
foreach ($ret as $car) {// Second approach
    $em->remove($car);
}

try {
    $em->flush();
} catch (\Exception $e) {
    $couldBeDeleted = Car::getDeletedEntities();
    $entityThatFailed = Car::getFailedToDeleteEntity();
    $notDeletedCars = Car::getNotDeletedEntities();

    // Do what you please, you can delete those entities that didn't fail though you'll have to reset the entitymanager (it'll be closed by now due to the exception).

    return $this->render('AcmeMyBundle:Car:errors.html.twig', array(// I'm going to respond with the ids that could've succeded, the id that failed and those entities that we don't know whether they could've succeeded or not.
                'deletedCars' => $couldBeDeleted,
                'failToDeleteCar' => $entityThatFailed,
                'notDeletedCars' => $notDeletedCars,
    ));
}

希望对您有所帮助。与第一种方法相比,实现起来有点麻烦,但在性能方面要好得多。

更新

我将尝试解释一下 catch 块中发生的事情:

此时交易失败。由于无法删除某些实体(例如由于 fk 约束),引发了异常。

事务已回滚,没有实体从数据库中实际删除。

$deletedCars 是一个变量,其中包含那些本来可以删除(它们没有引发任何异常)但没有删除(因为回滚)的实体的 ID。

$failToDeleteCar 包含删除引发异常的实体的 ID。

$notDeletedCars 包含交易中的其余实体 ID,但我们不知道是否会成功。

此时,您可以重置 entitymanager(它已关闭),使用未引起问题的 ID 启动另一个查询并删除它们(如果您愿意)并发回一条消息让用户知道您删除了那些实体,$failToDeleteCar 失败且未被删除,$notDeletedCars 也未被删除。由您决定要做什么。

我无法重现你提到的关于 Entity::getDeletedEntities() 的问题,它在这里工作正常。

您可以优化您的代码,这样您就不需要将此方法添加到您的实体(甚至生命周期回调)。例如,您可以使用订阅者来捕获事件,并使用带有静态方法的特殊 class 来跟踪那些没有失败的实体、失败的实体和没有机会的实​​体成为 deleted/updated/inserted。请参考我提供的文档。它比听起来要复杂一些,无法在几行代码中给你一个通用的答案,抱歉,你必须进一步调查。

我的建议是您尝试使用我提供的带有假实体的代码并进行一些测试以完全了解它是如何工作的。然后你可以尝试将它应用到你的实体中。

祝你好运!