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

标签 php mysql symfony doctrine-orm

这个问题在我脑海中盘旋了一段时间,现在我需要一些关于 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 用户在 this 上所说的那样answer execute flush() 是一种反模式,也会影响应用程序性能,因为每次都需要执行多个查询,但使用这种方法我可以得到哪个被插入,哪个没有被插入。

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

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

这个问题与this有密切关系和 this

现实生活中的例子

既然 @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 应该放在执行 DELETE 的 AppBundle:FabricanteProductoSolicitud 中,对吗?

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

已回答here自己使用用户评论作为输入,希望它可以帮助别人

通过@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你可以使用。为了简单起见,我将解释如何进行删除。同样为了简单起见,我将使用一个静态数组(它可以通过其他方式完成,我喜欢这个)和 lifecycle callbacks .在这种情况下,回调将是非常简单的方法(这就是为什么可以使用它们而不是实现 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 事件很重要,因为我们想知道是哪个实体导致交易失败。

现在,在 Controller 中我们可以这样做:

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 block 中发生的事情:

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

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

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

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

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

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

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

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

我的建议是您尝试我提供的带有假实体的代码,并进行一些测试以完全了解它的工作原理。然后您可以尝试将其应用于您的实体。

祝你好运!

关于php - 使用 preRemove/postRemove 事件来获取哪些查询可以执行,哪些不可以,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28097800/

相关文章:

php - 在 codeigniter 中找不到我的核心类 My_head

php - dompdf内存问题

php - 模型中的 Laravel 5 验证

php - PHP MySQL 中两次之间的持续时间

php - 使用php和mysql登录表单

spring - Symfony2 和 SpringMVC 上下文中的 XML 和 YML

php - 创建 QueryBuilder 以查找字段 ManuToMany

php - Laravel Restfull Controller 和路由 ajax/sync 请求

php - 使用php将csv文件上传到sql数据库的更好方法

symfony - 如何在事件监听器中利用 kernel.terminate