Using preRemove/postRemove events to get which que

2019-02-20 17:14发布

I'm having this question round in my mind for a while and now I need some kind of advice around preRemove/postRemove events since the queries I'll execute basically will be DELETE but this should apply also to prePersist/postPersist and preUpdate/postUpdate (doesn't know if those latest really exists).

I have this two possible situation for execute DELETE in several entities (see foreach loop):

// 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();

The first approach is not recommended and also as @acontell user said on this answer execute flush() is an antipatern and also will hit application performance since multiple queries will need to be executed each time but using that approach I can get which one was inserted and which one doesn't.

Using second approach I'll avoid antipatern and will improve performance but how do I know which item was inserted and which doesn't? Also if any query fails by default Doctrine will do a rollback so none will be inserted.

So, can I use preRemove/postRemove events to get which queries can be executed and which can't meaning which values are inserted or not?

This question has closed relation to this and this ones.

Real life example

Since @acontell give me another excellent answer I'll need some advice to see if I get the whole thing or I'm still lost so here is a real life example:

        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;
        }
    }

If I get it, then the LifeCycleCallbacks should go in AppBundle:FabricanteProductoSolicitud where DELETE are performed, is that right?

EDIT: Also I like to know the best approach to use the code on several entities since I'll have this behavior in most of them, then should be fine to define a Trait for this purpose? Should be defined as any other Trait?

This has been answered here by myself using users comments as input, hope it can help others

Performing some test on the code by @acontell

This is how my code looks like at this moment:

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);
}

The problem Entity\Producto::getDeletedEntities() is returning an empty array without deleted values, why?

1条回答
狗以群分
2楼-- · 2019-02-20 17:36

Here's how I'd do it. I'm not saying this is the best approach, if anyone knows something easier or better, I'd be the first interested in learning it.

First off, these are the 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).

Let's say we have this entity:

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]

As you can see, I've defined two callbacks that will be triggered with the preRemove event and the postRemove event.

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.

Then the php code of the entity:

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();
    }
}

Note the callbacks and the static arrays and methods. Everytime a remove is called over a Car entity, the preRemove callback will store the id of the entity in the array $preDeletedEntities. When the entity is deleted, the postRemove event will store the id in $entityDeleted. The preRemove event is important because we want to know which entity made the transaction fail.

And now, in controller we can do this:

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,
    ));
}

Hope it helps. It's a bit more cumbersome to implement than the first approach but much much better in terms of performance.

UPDATE

I'm going to try to explain a little bit more what's hapenning inside the catch block:

At this point, the transaction has failed. An exception has been raised due to the fact that the deletion of some entity is not possible (due for example to a fk constraint).

The transaction has been rolled back and no entites have been actually removed from the database.

$deletedCars is a variable that contains the ids of those entities that could've been deleted (they didn't raise any exception) but aren't (because of the roll back).

$failToDeleteCar contains the id of the entity whose deletion raised the exception.

$notDeletedCars contains the rest of the entities ids that were in the transaction but that we don't know wheter would have succeeded or not.

At this point, you can reset the entitymanager (it's closed), launch another query with the ids that didn't cause problem and delete them (if you like) and send back a message letting the user know that you deleted those entities and that $failToDeleteCar failed and wasn't deleted and $notDeletedCars weren't deleted either. It's up to you to decide what to do.

I can't reproduce the problem you mention about Entity::getDeletedEntities(), it's working fine here.

You could refine your code so that you didn't need to add this methods to your entities (not even the lifecycle callbacks). You could, for example, make use of a subscriber to capture events and a special class with static methods to keep track of those entities that didn't fail, the one that failed and those that didn't have the opportunity to be deleted/updated/inserted. I refer you to the documentation I provided. It's a bit more complicated than it sounds, not able to give you a generic answer in a few lines of code, sorry, you'll have to investigate further.

My suggestion is that you try the code I provided with a fake entity and make some tests to fully understand how it works. Then you can try to apply it to your entities.

Good luck!

查看更多
登录 后发表回答