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?
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:
As you can see, I've defined two callbacks that will be triggered with the preRemove event and the postRemove event.
Then the php code of the entity:
Note the callbacks and the static arrays and methods. Everytime a remove is called over a
Car
entity, thepreRemove
callback will store the id of the entity in the array$preDeletedEntities
. When the entity is deleted, thepostRemove
event will store the id in$entityDeleted
. ThepreRemove
event is important because we want to know which entity made the transaction fail.And now, in controller we can do this:
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!