If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTenemos un extraño problema: sabemos que AddPonkaToImageHandler
está siendo llamado con éxito por el proceso trabajador...., ¡porque realmente está añadiendo Ponka a las imágenes! Pero, por alguna razón... aunque llamemos a$imagePost->markAsPonkaAdded()
... que establece la propiedad $ponkaAddedAt
... y luego a $this->entityManager->flush()
... ¡no parece que se esté guardando!
Así que... podrías preguntarte:
¿Tengo que llamar a persist() en
$imagePost
?
Vamos a probarlo: $this->entityManager->persist($imagePost)
. En teoría, no deberíamos necesitarlo: sólo hay que llamar a persist()
en los objetos nuevos que quieras guardar. No es necesario... y normalmente no hace nada... cuando lo llamas en un objeto que se va a actualizar.
Pero... qué demonios... veamos qué ocurre.
Pero antes de intentar esto... ¡tenemos que hacer algo muy importante! Busca tu terminal, pulsa Ctrl+C para detener el trabajador y luego reinícialo:
php bin/console messenger:consume
¿Por qué? Como sabes, los trabajadores se quedan ahí y se ejecutan... para siempre. El problema es que, si actualizas algo de tu código, ¡el trabajador no lo verá! Hasta que lo reinicies, ¡sigue teniendo el código antiguo almacenado en la memoria! Así que cada vez que hagas un cambio en el código que utiliza un trabajador, asegúrate de reiniciarlo. Más adelante, hablaremos de cómo hacer esto de forma segura al desplegar.
Veamos qué ocurre ahora que hemos añadido esa nueva llamada a persist()
. Sube un nuevo archivo, encuentra tu trabajador y... ¡sí! Se ha gestionado con éxito. ¿Se ha solucionado el problema de guardar la entidad? Actualiza la página.
¡Vaya! ¡Qué acaba de pasar! la imagen aparece dos veces Una con la fecha puesta... y otra sin ella. ¡En la base de datos!
SELECT * FROM image_post \G
Sí... esta única imagen está en dos filas: Lo sé porque apuntan exactamente al mismo archivo en el sistema de archivos. El trabajador... de alguna manera... duplicó esa fila en la base de datos.
Este... es un error confuso... pero tiene una fácil solución. Primero, veamos las cosas desde la perspectiva de Doctrine. Internamente, Doctrine mantiene una lista de todos los objetos de entidad con los que está tratando en ese momento. Cuando consultas una entidad, la añade a esta lista. Cuando llamas a persist()
, si no está ya en la lista, la añade. Luego, cuando llamamos a flush()
, Doctrine recorre todos estos objetos, busca los que han cambiado y crea las consultas UPDATE o INSERT adecuadas. Sabe si un objeto debe insertarse o actualizarse porque sabe si fue responsable de la consulta de ese objeto. Por cierto, si quieres empollar más este tema, esta "lista" se llama mapa de identidades... y no es más que una gran matriz que empieza vacía al principio de cada petición y se va agrandando a medida que se hacen consultas o se guardan cosas.
Así que ahora pensamos en lo que ocurre en nuestro trabajador. Cuando se deserializa el objeto AddPonkaToImage
, también se deserializa el objeto ImagePost
que vive dentro. En ese momento, el mapa de identidad de Doctrine no contiene este objeto... porque no lo ha consultado dentro de este proceso PHP - desde dentro del trabajador. Por eso, originalmente, antes de añadir persist()
, cuando llamábamos aflush()
, Doctrine miró la lista de objetos de su mapa de identidad -que estaba vacía- y... no hizo absolutamente nada: ¡no sabe que debe guardar el ImagePost
!
Cuando añadimos persist()
, creamos un problema diferente. Ahora Doctrine es consciente de que debe guardarlo... pero como no lo consultó originalmente, piensa erróneamente que debe insertarlo en la base de datos como una nueva fila, en lugar de actualizarlo.
¡Uf! Quería que vieras esto porque... es un poco difícil de depurar. Afortunadamente, la solución es fácil. Y toca una importante práctica recomendada para tus mensajes: incluye sólo la información que necesitas. Eso a continuación.
Hey erop
As long as all handlers won't update the entity I think it could work but can you guarantee that? The second possible problem I see is a race condition, what if the user modified an important field before all of your handlers were processed? It may not be a problem but is something to consider
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // v1.8.0
"doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
"doctrine/orm": "^2.5.11", // v2.6.3
"intervention/image": "^2.4", // 2.4.2
"league/flysystem-bundle": "^1.0", // 1.1.0
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
"sensio/framework-extra-bundle": "^5.3", // v5.3.1
"symfony/console": "4.3.*", // v4.3.2
"symfony/dotenv": "4.3.*", // v4.3.2
"symfony/flex": "^1.9", // v1.18.7
"symfony/framework-bundle": "4.3.*", // v4.3.2
"symfony/messenger": "4.3.*", // v4.3.4
"symfony/property-access": "4.3.*", // v4.3.2
"symfony/property-info": "4.3.*", // v4.3.2
"symfony/serializer": "4.3.*", // v4.3.2
"symfony/validator": "4.3.*", // v4.3.2
"symfony/webpack-encore-bundle": "^1.5", // v1.6.2
"symfony/yaml": "4.3.*" // v4.3.2
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.7", // v1.0.7
"symfony/debug-bundle": "4.3.*", // v4.3.2
"symfony/maker-bundle": "^1.0", // v1.12.0
"symfony/monolog-bundle": "^3.0", // v3.4.0
"symfony/stopwatch": "4.3.*", // v4.3.2
"symfony/twig-bundle": "4.3.*", // v4.3.2
"symfony/var-dumper": "4.3.*", // v4.3.2
"symfony/web-profiler-bundle": "4.3.*" // v4.3.2
}
}
Hi everyone!
In my scenario I'm still going to pass the whole entity to async transport. The command is dispatched inside decorated data persister of ApiPlatform after persisting the entity. I think it's quite reasonable in my case since a) there will be about 20+ command handlers for that command (i.e. all of them should individually retrieve entity from database) and b) there will not be any entity updating inside those handler. Just "fire and forget"! It seems OK at first sight. But I wonder are there any drawbacks in this approach that could hit me in the future.