Symfony/Doctrine not updating in memory - php

I suspect this is a straightforward problem about entities not updating in memory.
Here are my repository functions:
public function insertTask($information)
{
...
$entityManager = $this->getEntityManager();
$entityManager->persist($taskObj);
$entityManager->flush();
// Update the task defects for this task
$this->updateTaskDefectTaskIds($taskObj->getId(), $task['defects']);
return $taskObj;
}
private function updateTaskDefectTaskIds($task, $defects)
{
$taskDefectIds = array();
foreach ($defects as $defect)
{
$taskDefectIds[] = intval($defect['taskDefectId']);
}
// Update the task IDs on the task defects
$this->getEntityManager()
->createQuery('UPDATE Model:TaskDefect td SET td.task = :task WHERE td.id IN (:taskDefectIds)')
->setParameter('task', $task)
->setParameter('taskDefectIds', $taskDefectIds)
->execute();
$this->getEntityManager()->flush();
}
This appears to be working. Basically I have:
- Task table
- TaskDefect table
TaskDefects by virtue of the way the app is written, get inserted before the Task does. But this means I need to, once the Task is eventually added, update the TaskDefect records to point to the new TaskId.
The problem I face though, is somewhere in memory (from the actual database side of things it is updating correctly) it's not picking up my changes. For example, I update some TaskDefects to point to the new TaskId - but then I access the Task object and it says there are no defects.
If I go to another page, and try and access the same task - then it says there are defects.
So I feel I'm missing a flush() or a persist() or something which is stopping the entities in memory from updating. Obviously reloading a page forces the refresh and it works fine then.
Here's what I have in my controller:
$task = $repository->insertTask($content); // i figured maybe at this point it's too much to expect the task obj to magically update
$updatedTask = $repository->findOneById($task->getId()); // so I grab it again... but no luck
var_dump($updatedTask->getDefects()); // ... because this returns no defects
Any ideas welcome. Thanks.

This line in insertTask:
return $taskObj;
Has no connection to what is happening in updateTaskDefectTaskIds because you're not modifying $taskObj at all, you're just passing an id value, then updating defect objects via DQL.
If you'd like $taskObj to reflect your defect additions from insertTask you would do something like this:
public function insertTask($information)
{
...
$entityManager = $this->getEntityManager();
$entityManager->persist($taskObj);
$entityManager->flush();
// Update the task defects for this task
$this->updateTaskDefectTaskIds($taskObj, $task['defects']);
return $taskObj;
}
private function updateTaskDefectTaskIds($taskObj, $defects)
{
foreach ($defects as $defect)
{
$defect = $this->getEntityManager()->getRepository('YourBundle:Defect')->find(intval($defect['taskDefectId']));
if ($defect instanceof Defect) {
$defect->setTaskObj($taskObj);
$this->getEntityManager()->persist($defect);
$taskObj->addDefect($defect);
}
}
$this->getEntityManager()->persist($taskObj);
$this->getEntityManager()->flush();
}
Or, if you don't mind an extra db call just refresh $taskObj in insertTask like this:
$this->getEntityManager()->refresh($taskObj);
return $taskObj;
Also, doctrine loves to cache what you have in memory, so if it doesn't have any reason to check the db (in your code example it can't know about your change) then it will just happily serve you up the stale object when you fetch the entity by id.

Related

Doctrine not updating properly entity data with several concurrent requests

I need to do something basic, I have two entities: User and Action. Each User has X tokens assigned by the Admin and then he can perform Y actions based on the amount of tokens. So lets say one User only has enough tokens to perform one Action, I identified that if I perform multiple simultaneously requests at the same exact time (like 5 or more requests at the same time). Instead of just one Action, the User executes two or more Actions (and only in the explained scenario, in the rest everything works fine)
The related code to my explanation:
public function useractions(Requests $request){
$user = $this->getUser();
$post = Request::createFromGlobals();
if($post->request->has('new_action') && $this->isCsrfTokenValid("mycsrf", $post->request->get('csrf_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$tokens = $user->getTokens();
if($tokens<1){
$error = "Not enough tokens";
}
if(empty($error)){
$user->setTokens($tokens-1);
$entityManager->flush();
$action = new Action();
$action->setUser($user);
$entityManager->persist($transaction);
$entityManager->flush();
}
}
}
And I am using mariadb 10.5.12 with InnoDB as the engine
Obviously I am making a big mistake in my code or missing something in the Symfony or Doctrine configuration. Someone could tell me the mistake? Thanks
You probably have a race condition between the user's SELECT (the user refresh, called by your authentication mechanism) and your UPDATE (the first $entityManager->flush();).
Doctrine has a built-in method to manage concurrent requests, the EntityManager::transactional() method (doc). You may just need to wrap your code into it:
$entityManager = $this->getDoctrine()->getManager();
$error = $entityManager->transactional(function($entityManager) use ($user) {
// Required to perform a blocking SELECT for UPDATE
$entityManager->refresh($user);
$tokens = $user->getTokens();
if($tokens<1){
return "Not enough tokens";
}
$user->setTokens($tokens-1);
$entityManager->persist($user);
});
if (empty($error)) {
$action = new Action();
$action->setUser($user);
$entityManager->persist($action);
$entityManager->flush();
}
Note: make sure that your transaction is fast enough, as it has blocking effects on the mariadb side, affecting the involved rows.
For the execution of one update command, you should use flush once, and not in each iterate.
also, you can use transactions to do all of them as one unit.
but if you want to prevent concurrency updating you should use versioning! by this, when doctrine wants to update something check versioning and if it changes throw an error and prevent to persist data to the database ( you can handle this error by a listener ) and by this way just first request will be a success.
for example, I flag updatedAt filed as a version filed and it will be checked in each persistence.
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Version
* #ORM\Column(type="datetime",options={"default": "CURRENT_TIMESTAMP"})
*/
private $UpdatedAt;

DDD - how to deal with get-or-create logic in Application Layer?

I have an DailyReport Entity in my Domain Layer. There are some fields in this object:
reportId
userId
date
tasks - Collection of things that user did in given day;
mood - how does the user felt during the whole day;
Also, there are some methods in my Application Service:
DailyReportService::addTaskToDailyReport
DailyReportService::setUserMoodInDailyReport
The thing is that both of these methods require DailyReport to be created earlier or created during function execution. How to deal with this situation?
I have found 2 solutions:
1 Create new DailyReport object before method dispatching, and after that pass reportId to them:
//PHP, simplified
public function __invoke() {
$taskData = getTaskData();
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser('1234-12-12', $user);
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport('1234-12-12', $user);
$dailyReportRepository->store($dailyReport);
}
$result = $dailyReportService->addTaskToDailyReport($taskData, $dailyReport->reportId);
//[...]
}
This one requires to put a more business logic to my Controller which i want to avoid.
2: Verify in method that DailyReport exists, and create new one if needed:
//my controller method
public function __invoke() {
$taskData = getTaskData();
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
//in my service:
public function addTaskToDailyReport($taskData, $date, $user) {
//Ensure that daily report for given day and user exists:
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser();
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport($date, $user);
$dailyReportRepository->store($dailyReport);
}
//perform rest of domain logic here
}
This one reduces complexity of my UI layer and does not expose business logic above the Application Layer.
Maybe these example is more CRUD-ish than DDD, but i wanted to expose one of my use-case in simpler way.
Which solution should be used when in these case? Is there any better way to handle get-or-create logic in DDD?
EDIT 2020-03-05 16:21:
a 3 example, this is what i am talking about in my first comment to Savvas Answer:
//a method that listens to new requests
public function onKernelRequest() {
//assume that user is logged in
$dailyReportService->ensureThereIsAUserReportForGivenDay(
$userObject,
$currentDateObject
);
}
// in my dailyReportService:
public function ensureThereIsAUserReportForGivenDay($user, $date) {
$report = getReportFromDB();
if($report === null) {
$report = createNewReport();
storeNewReport();
}
return $report;
}
//in my controllers
public function __invoke() {
$taskData = getTaskData();
//addTaskToDailyReport() only adds the data to summary, does not creates a new one
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
This will be executed only when user will log in for the first time/user were logged in yesterday but this is his first request during the new day.
There will be less complexity in my business logic, i do not need to constantly checking in services/controllers if there is a report created because this has been executed
previously in the day.
I'm not sure if this is the answer you want to hear, but basically I think you're dealing with accidental complexity, and you're trying to solve the wrong problem.
Before continuing I'd strongly suggest you consider the following questions:
What happens if someone submits the same report twice
What happens if someone submits a report two different times, but in the second one, it's slightly different?
What is the impact of actually storing the same report from the same person twice?
The answers to the above questions should guide your decision.
IMPORTANT: Also, please note that both of your methods above have a small window where two concurrent requests to store the rerport would succeed.
From personal experience I would suggest:
If having duplicates isn't that big a problem (for example you may have a script that you run manually or automatically every so often that clears duplicates), then follow your option 1. It's not that bad, and for human scale errors should work OK.
If duplicates are somewhat of a problem, have a process that runs asynchronously after reports are submited, and tries to find duplicates. Then deal with them according to how your domain experts want (for example maybe duplicates are deleted, if one is newer either the old is deleted or flagged for human decision)
If this is part of an invariant-level constraint in the business (although I highly doubt it given that we're speaking about reports), and at no point in time should there ever be two reports, then there should be an aggregate in place to enforce this. Maybe this is UserMonthlyReport or whatever, and you can enforce this during runtime. Of course this is more complicated and potentially a lot more work, but if there is a business case for an invariant, then this is what you should do. (again, I doubt it's needed for reports, but I write it here in the care reports were used as an example, or for future readers).

Get auto generated ID right after flush() Symfony3

I have a Project entity and when creating a new project I need to be able to upload files. For this reason I have created Files entity which saves the path and projectID. My problem is that I don't know how to retrieve it after creating it. Here is what I am trying to do
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
Currently my $project doesn't have ID which means that I cant create a new file. I heard that I could use $em->retrieve(object), but the actual flushing is not done in the controller. If i try to use it in my createProject function and return it for some reason I can't. PHPStorm says that it is a void function. Here is the code in createProject
$project->setFromUser($user->getFullName());
$project->setDepartment($user->getDepartment());
$project->setIsOver(false);
$project->setDate(new \DateTime());
if($isWithoutTerm){
$project->setTerm(\DateTime::createFromFormat('Y-m-d', $noTermDefaultValue));
}
$this->entityManager->persist($project);
$this->entityManager->flush();
Is there a way to retrieve the projectID after flushing and being able to use it in my controller?
It could be a race condition where the $project is not available yet and so the service cannot load it and thus getId() fails.
First, create your project entity:
//build your $project
$this->entityManager->flush();
Second, send this $project to your "service"
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
More specifically if you wrap this in a try/catch you can identify exactly what is happening:
try {
$project = new Project();
//build project
$this->entityManager->flush();
//load $projectService..
$projectService->createProject($project,$user,$isWithoutTerm,self::NO_TERM_DEFAULT_VALUE);
$filesService = $this->get('app.service.files_service');
foreach ($managerFiles as $managerFile){
$fileName = $filesService->uploadFileAndReturnName($managerFile);
$filesService->createFile($fileName,$project->getId(),$user);
}
}
catch(\Exception $e) {
//$e->getMessage() will tell you if you're good to go, or if there
//is actually an issue
}
However, it could also be as simple as PHPStorm not being able to introspect your getId() method and thus doesn't understand it. OR getId() is not actually returning the id.
Upgrading the Symfony from 3.2 and 3.3 fixed the issue. I don't think it was due to the version itself, but rather removing depricated stuff and changing a lot of things and something fixed it.

Concurrent requests trying to create the same entity if not exists in Doctrine

I have a function that looks like this:
function findByAndCreateIfNotExists($criteria){
$entity = $this->findBy(['criteria'=>$criteria]);
// this is the problem area, if request 1 is still creating the entity, request 2 won't find it yet.
if (! $entity) {
$entity = $this->createEntity($criteria);
}
return $entity;
}
This function is used by various requests, and I've found that concurrent requests will sometimes try to create the same entity, throwing a DBALException complaining about duplicate entries for the unique key.
I've thought about LOCK TABLES as mentioned here:
How to lock a whole table in symfony2 with doctrine2?
But just given the fact there is no function to do this in Doctrine, I'm guessing it's not the preferred method. My question is, how can I prevent concurrent requests attempting to create the same entity and always return the correct one?
As idea create own locking system:
something like:
function findByAndCreateIfNotExists($criteria){
$entity = $this->findBy(['criteria'=>$criteria]);
// this is the problem area, if request 1 is still creating the entity, request 2 won't find it yet.
$lockPath ='/tmp/'.md5($criteria).'.lock';
if (! $entity && !file_exists($lockPath)) {
$fh = fopen($lockPath, 'w') or die("Can't create file");
$entity = $this->createEntity($criteria);
unlink(($lockPath);
}
return $entity;
}
But from what you told I think you are doing in wrong direction and it is better to rebuild app architecture little bit and put RabbitMQ queue as intermediate point to serve your requests

How can I tell if the current transaction will change any entities with Doctrine 2?

I'm using Doctrine to save user data and I want to have a last modification field.
Here is the pseudo-code for how I would like to save the form once the user presses Save:
start transaction
do a lot of things, possibly querying the database, possibly not
if anything will be changed by this transaction
modify a last updated field
commit transaction
The problematic part is if anything will be changed by this transaction. Can Doctrine give me such information?
How can I tell if entities have changed in the current transaction?
edit
Just to clear things up, I'm trying to modify a field called lastUpdated in an entity called User if any entity (including but not limited to User) will be changed once the currect transaction is commited. In other words, if I start a transaction and modify the field called nbCars of an entity called Garage, I wish to update the lastUpdated field of the User entity even though that entity hasn't been modified.
This is a necessary reply that aims at correcting what #ColinMorelli posted (since flushing within an lifecycle event listener is disallowed - yes, there's one location in the docs that says otherwise, but we'll get rid of that, so please don't do it!).
You can simply listen to onFlush with a listener like following:
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
class UpdateUserEventSubscriber implements EventSubscriber
{
protected $user;
public function __construct(User $user)
{
// assuming the user is managed here
$this->user = $user;
}
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// before you ask, `(bool) array()` with empty array is `false`
if (
$uow->getScheduledEntityInsertions()
|| $uow->getScheduledEntityUpdates()
|| $uow->getScheduledEntityDeletions()
|| $uow->getScheduledCollectionUpdates()
|| $uow->getScheduledCollectionDeletions()
) {
// update the user here
$this->user->setLastModifiedDate(new DateTime());
$uow->recomputeSingleEntityChangeSet(
$em->getClassMetadata(get_class($this->user)),
$this->user
);
}
}
public function getSubscribedEvents()
{
return array(Events::onFlush);
}
}
This will apply the change to the configured User object only if the UnitOfWork contains changes to be committed to the DB (an unit of work is actually what you could probably define as an application level state transaction).
You can register this subscriber with the ORM at any time by calling
$user = $entityManager->find('User', 123);
$eventManager = $entityManager->getEventManager();
$subscriber = new UpdateUserEventSubscriber($user);
$eventManager->addEventSubscriber($subscriber);
Sorry for giving you the wrong answer at first, this should guide you in the right direction (note that it's not perfect).
You'll need to implement two events. One which listens to the OnFlush event, and acts like this:
// This should listen to OnFlush events
public function updateLastModifiedTime(OnFlushEventArgs $event) {
$entity = $event->getEntity();
$entityManager = $event->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
if (count($unitOfWork->getScheduledEntityInsertions()) > 0 || count($unitOfWork->getScheduledEntityUpdates()) > 0) {
// update the user here
$this->user->setLastModifiedDate(new \DateTime());
}
}
We need to wait for the OnFlush event, because this is the only opportunity for us to get access to all of the work that is going to be done. Note, I didn't include it above, but there is also $unitOfWork->getScheduledEntityDeletions() as well, if you want to track that.
Next, you need another final event listener which listens to the PostFlush event, and looks like this:
// This should listen to PostFlush events
public function writeLastUserUpdate(PostFlushEventArgs $event) {
$entityManager = $event->getEntityManager();
$entityManager->persist($this->user);
$entityManager->flush($this->user);
}
Once the transaction has been started, it's too late, unfortunately, to get doctrine to save another entity. Because of that, we can make the update to the field of the User object in the OnFlush handler, but we can't actually save it there. (You can probably find a way to do this, but it's not supported by Doctrine and would have to use some protected APIs of the UnitOfWork).
Once the transaction completes, however, you can immediately execute another quick transaction to update the datetime on the user. Yes, this does have the unfortunate side-effect of not executing in a single transaction.
#PreUpdate event won't be invoked if there's no change on the entity.
My guess would have been, similarly to the other 2 answers, is to say whatever you want to do when there will or will not be changes, use event listeners.
But if you only want to know before the transaction starts, you can use Doctrine_Record::getModified() (link).

Categories