doctrine ODM (mongodb) merge doesn't work - php

I'm not having any luck using merge(). I'm doing almost exactly what is documented:
/* #var $detachedDocument MyDocumentClass */
$detachedDocument = unserialize($serializedDocument);
$document = $dm->merge($detachedDocument);
$document->setLastUpdated(new \MongoDate());
$dm->persist($document);
but the change never sticks. I have to do this instead:
$dm->createQueryBuilder('MyDocumentClass')
->findAndUpdate()
->field('lastUpdated')->set(new \MongoDate())
->getQuery()
->execute();
merge() seems pretty straightforward, so I'm confused why it doesn't work like I think it should.

In your first code example, merge() followed by persist() is redundant, and you omitted a flush(), which is the only operation that would actually write to the database (unless you execute a query manually, as you did in the second example). If you walk through the code in UnitOfWork::doMerge(), you'll see that it's going to either persist the object (if it has no ID) or fetch the document by its ID. The end result is that merge() returns a managed document. Persist ensures that the document will be managed after it is called (it returns nothing itself). If you poke in UnitOfWork::doPersist(), you'll see that passing a managed object to the method is effectively a NOOP.
Try replacing persist() with flush(). Note that you can flush a single document if necessary, but $dm->flush() processes all managed objects by default.
If that still doesn't help, I'd confirm that the lastUpdated field is properly mapped in ODM. You can inspect the output of $dm->getClassMetadata('MyDocumentClass') to confirm. If it isn't a mapped field, UnitOfWork will detect no changes in the document and there will be nothing to flush.
As an aside: in the second code example, you're executing findAndUpdate() without any search criteria (only the set() is specified). Typically, you'd pair the modification with something like equals() (probably the ID in your case) to ensure that a single document is atomically modified and returned.

Related

Scheduled entity in onFlush is different instance

I have a strange problem with \Doctrine\ORM\UnitOfWork::getScheduledEntityDeletions used inside onFlush event
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof PollVote) {
$arr = $entity->getAnswer()->getVotes()->toArray();
dump($arr);
dump($entity);
dump(in_array($entity, $arr, true));
dump(in_array($entity, $arr));
}
}
And here is the result:
So we see that the object is pointing to a different instance than the original, therefore in_array no longer yields expected results when used with stick comparison (AKA ===). Furthermore, the \DateTime object is pointing to a different instance.
The only possible explanation I found is the following (source):
Whenever you fetch an object from the database Doctrine will keep a copy of all the properties and associations inside the UnitOfWork. Because variables in the PHP language are subject to “copy-on-write” the memory usage of a PHP request that only reads objects from the database is the same as if Doctrine did not keep this variable copy. Only if you start changing variables PHP will create new variables internally that consume new memory.
However, I did not change anything (even the created field is kept as it is). The only operations that were preformed on entity are:
\Doctrine\ORM\EntityRepository::findBy (fetching from DB)
\Doctrine\Common\Persistence\ObjectManager::remove (scheduling for removal)
$em->flush(); (triggering synchronization with DB)
Which leads me to think (I might be wrong) that the Doctrine's change tracking method has nothing to do with the issue that I'm experiencing. Which leads me to following questions:
What causes this?
How to reliably check if an entity scheduled for deletion is inside a collection (\Doctrine\Common\Collections\Collection::contains uses in_array with strict comparison) or which items in a collection are scheduled for deletion?
The problem is that when you tell doctrine to remove entity, it is removed from identity map (here):
<?php
public function scheduleForDelete($entity)
{
$oid = spl_object_hash($entity);
// ....
$this->removeFromIdentityMap($entity);
// ...
if ( ! isset($this->entityDeletions[$oid])) {
$this->entityDeletions[$oid] = $entity;
$this->entityStates[$oid] = self::STATE_REMOVED;
}
}
And when you do $entity->getAnswer()->getVotes(), it does the following:
Load all votes from database
For every vote, checks if it is in identity map, use old one
If it is not in identity map, create new object
Try to call $entity->getAnswer()->getVotes() before you delete entity. If the problem disappears, then I am right. Of cause, I would not suggest this hack as a solution, just to make sure we understand what is going on under the hood.
UPD instead of $entity->getAnswer()->getVotes() you should probably do foreach for all votes, because of lazy loading. If you just call $entity->getAnswer()->getVotes(), Doctrine probably wouldn't do anytning, and will load them only when you start to iterate through them.
From the doc:
If you call the EntityManager and ask for an entity with a specific ID twice, it will return the same instance
So calling twice findOneBy(['id' => 12]) should result in two exact same instances.
So it all depends on how both instances are retrieved by Doctrine.
In my opinion, the one you get in $arr is from a One-to-Many association on $votes in the Answer entity, which results in a separate query (maybe a id IN (12)) by the ORM.
Something you could try is to declare this association as EAGER (fetch="EAGER"), it may force the ORM to make a specific query and keep it in cache so that the second time you want to get it, the same instance is returned ?
Could you have a look at the logs and post them here ? It may indicates something interesting or at least relevant to investigate further.

Doctrine2: How to handle missing relation documents

within a huge dataset I sometimes get inconsistencies when one document is deleted. Symfony2 App with Doctrine ODM and FosREST
$a = new Element();
$b = new Element();
$c = new List();
$c->addElement($a);
$c->addElement($b);
$em->persist($c);
saving at this point works flawlessly
in 99% of the cases $a and $b are still valid Documents when $c is loaded later.
BUT sometimes either $a or $b is deleted without updating the reference in $c.
-> at this moment the next loading of $c will fail with a \Doctrine\ODM\MongoDB\DocumentNotFoundException
(message is something like: The "MongoDBODMProxies__CG__\App\Model\Element" document with identifier "541417702798711d2900607c" could not be found.)
What is the best approach now to handle this case?
I was thinking about either
catching the Exception and to check if the reference it tried to load was on the Element Model
custom exception Handler in fosRest to check for
custom repository function in the mapping and to check there if everything is still valid (+ to store somehow that there is a missing Element) -> but this then forces me to check on every occasion if the "error" is set
UPDATE: The Mapping between the Documents is a bit more complex than I described here
for one the element is basically a collection separated by a discriminator, where only one type of fields references another document (I call it "Tree" now)
a tree can be used in thousands of ElementTree's (that specific type that contains a Tree)
sometimes Tree's can be deleted (this is already a slow running process since a lot of data needs to be updated then)
I would now need to find out what Lists need to change and basically reject the api calls to those lists with the information that a specific element is no longer available.
A few things to check especially for MongoDB:
Make sure that there are no circular references (for example if you have the property $elements on the class List and references set to true on it, make sure List is not referenced on the Elements class as well) and your mappings are consistent.
In the addElement function IF the reference is held on the Element class make sure you also call $element->setList($this) inside the function. (and the same for removeElement, unset the reference if neccessary)
Make sure you cascade all the necessary operations. (For example cascade : ["persist", "delete", "refresh" or "all" ]
You can check your mappings with
$ app/console doctrine:mongodb:mapping:info
Finally if you expect that document to be deleted but you get an error from the proxy object you can clear the metadata cache
$ app/console doctrine:mongodb:cache:clear-metadata
Inperfect Solution that works for now
I now chose to throw a new Exception (it is important not let doctrine throw one because it will reject then any persist attempts in the same request).
In the PostLoad LifecycleEvent I check now the following (simplified):
if ($document instanceof List) {
foreach ($document->getElements() as $element) {
// at this moment $element->getId() is already defined but not yet loaded from mongo
$result = $this->elementRepository->findBy(array(‘_id’ => $element->getId()));
if (sizeof($result)==0) {
throw new InvalidElementInList($element->getId());
}
}
}
in the RestController this enables me now to catch this specific exception and to remove the invalid element from the list + to return a custom view to the user indicating that the element was removed.

ZF2: Where does the controller return/result get manipulated by the MVC?

I am working on a RESTful API in Zend Framework 2. I use the AbstractRestfulController to handle requests. In that controller I want to directly return an array or object (of a custom model class), not a JsonModel. That stuff and the conversion of the result to arrays (as a preparation for the JSON encoding) should happen later automatically.
I tried to catch the MvcEvent::EVENT_DISPATCH event to manipulate the $e->getResult()value but when I return an associative array in the controller I get a ViewModel in the result instead of the plain data passed in by the controller method.
I tried to create a custom view strategy by implementing the ListenerAggregateInterface class. In the ViewEvent::EVENT_RESPONSE event catch, I dumped the $e->getResult() value and got the already encoded result. Also here, I need the plain result to prepare it for encoding.
Where can I hook in to manipulate the controller's plain return value before encoding to JSON? Where does ZF2 generally manipulate the returned value in the MVC lifecycle?
Thank you in advance!
I don't have so much time to investigate in this issue, but my guess is that you may omit to stop event propagation when you attached a listener to MvcEvent::EVENT_DISPATCH. So what you set as the event result is probably overridden later in the dispatch process.
Try to attach your callback with a higher priority and stop the propagation ($e->stopPropagation() from the callback), since the event manager won't stop the propagation by itself until one or the other callback returns a Zend\StdLib\ResponseInterface object.
Anyway, I know you probably have good reasons trying to "hack" the dispatch process this way, but I tend to think that there could be even better reasons to adapt your code to fit the default process ;)
Here, zf2 creates a ViewModel in case an assoc array is detected in the result, registered at priority -80. According to this documentation page this is the first event catch changing the result returned by the controller.
What I did wrong is attaching my MvcEvent::EVENT_DISPATCH to the $application->getEventManager() instead of $application->getEventManager()->getSharedManager() (like zf2 does). I don't understand yet what the difference is, but it works.
Here is how I registered the event in the onBootstrap method:
$application->getEventManager()->getSharedManager()->attach(
__NAMESPACE__,
MvcEvent::EVENT_DISPATCH,
array($this, 'createJsonModelFromResult'),
-10
// priority of -10 because at priority 1 the mvc
// runs the actual controller method
);
At the end of the createJsonModelFromResult method I also added $e->stopPropagation(); because we already found the matching ViewModel.
Now the $e->getResult() value is exactly the same as returned in my controller actions and I can prepare it for JSON encoding. Cool!
I have no idea how you want to return an array or Object. Usually people make a DTO (Data Transfer Object), serialize it, and then send it. In this method the DTO at the end converts to JSON.
What format do you want to use in response? String (serialized DTO), XML, or something else? You can create a class extending ViewModel and serialize your array for your format.
Anyway, you do not need to get MVC_EVENT.

Doctrine 2: Force scheduleForUpdate on a non-changed entity

How can I schedule an entity for update, manually, when no property is actually changed?
I tried $entityManager->getUnitOfWork()->scheduleForUpdate($entity) but it gave an error in the core, and I have no intetion of debuging Doctrine.
The entity is managed if it matters: $entity = $repository->findOne(1)
I need this so doctrine would call my EventSubscriber on flush().
I've also tried something like $entityManager->getEventManager()->dispatchEvent(\Doctrine\ORM\Events::preUpdate), but then my listener's preUpdate() receives EventArgs instead of PreUpdateEventArgs.
Any help is appreciated!
Method mentioned by Wpigott not working for me (at least in doctrine/orm v2.4.2), instead I'm using this:
$entityManager->getUnitOfWork()->setOriginalEntityProperty(spl_object_hash($entity), 'field_name', '__fake_value__');
Where field_name existent property name.
The solution is a bit hacky, but I was able to achieve this by doing something like the following.
$objectManager->getUnitOfWork()->setOriginalDocumentData($object, array('__fake_field'=>'1'));
This essentially causes Doctrine to think the document has changed from the original, and it computes it as a change which will cause the events to be executed on flush.
The example is for the MongoODM solution, but the same technique should work for ORM like below.
$objectManager->getUnitOfWork()->setOriginalEntityData($object, array('__fake_field'=>'1'));
Even though this question is quite a bit old, I just found a way much more elegant to solve this problem I want to share here using Doctrine ORM 2.6.2:
You can simply tell the ClassMetadataInfo object of your table to just state those fields as dirty that you pass to the propertyChanged function of the UnitOfWork. The important parts here are the setChangeTrackingPolicy and propertyChanged calls:
$unitOfWork = $entityManager->getUnitOfWork();
$classMeta = $entityManager->getClassMetadata('Your\Table\Class');
$ctp = $classMeta->changeTrackingPolicy; # not a function but a variable
# Tell the table class to not automatically calculate changed values but just
# mark those fields as dirty that get passed to propertyChanged function
$classMeta->setChangeTrackingPolicy(ORM\ClassMetadataInfo::CHANGETRACKING_NOTIFY);
# tell the unit of work that an value has changed no matter if the value
# is actually different from the value already persistent
$oldValue = $entity->getValue('fieldName'); # some custom implementation or something
$newValue = $oldValue;
$unitOfWork->propertyChanged($entity, 'fieldName', $oldValue, $newValue);
# field value will be updated regardless of whether its PHP value has actually changed
$entityManager->flush();
# set the change tracking policy back to the previous value to not mess things up
$classMeta->setChangeTrackingPolicy($ctp);
You may also want to have a look at the Doctrine\Common\NotifyPropertyChanged interface.
I hope this will be useful for someone.
What's the goal? If there is no property changed, why would you plan an update?

How do i detach a behavior in Symfony/Doctrine?

I have doctrine's softdelete behavior attached to all of my models. Is there a way I can hard delete a particular record?
In cakephp I remember detaching the behavior... deleting the record and then re attaching the behavior.
Is there something similar in symfony/doctrine ? If so then how do I detach a behavior?
Cheers
umm .. the SoftDelete behavior includes a much nicer way of doing this ... just call
$record->hardDelete();
Think I'd go for Zed's way, but for completeness:
The Event listener method for delete (and select) for the soft delete behaviour contains:
if ( ! $query->contains($field)) {
// do the magic stuff to covert the query to respect softdelete
}
This means that if you explicitly mention the field in the query, it won't apply the transformation to the query.
So, if you do:
$q = Doctrine_Query::create()
->delete('Table t')
->where('t.id = ? AND t.deleted != 2 ', 1);
it won't apply the soft delete stuff and will actually delete the record. Note that you can do anything with t.deleted, I've just done something that will always be true. The alias ('t.') is important too for it to work.
This trick works for selects too, which is where I've normally used it before.
As I say though, I think its nicer to do:
$old_dqlc = Doctrine_Manager::getInstance()->getAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS);
Doctrine_Manager::getInstance()->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, false);
$record->delete();
Doctrine_Manager::getInstance()->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, $old_dqlc);
In particular, you can still use the delete() method rather than having to manually create the query. The one plus for the query method is that if you have other behaviours attached to the record, they will still be respected.
$object->getListener()->setOption('disabled',true);
This will disable all record listeners for this object.
Try calling this, it should disable the behavior handling.
$manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, false);
As a dirty way you can generate an SQL query that deletes the entry from the table.
link text i would think that this function and setting the use dql callbacks to false just like on the manager should do the trick :).
Wanted to agree with Joshua Coady that the best way would be to use
$record->hardDelete()
However, I also wanted to add here since it's one of the first results on google for detaching the behavior in doctrine that the easiest way to detach the behavior for "selects" is simply to include "deleted_at" (or whatever you have named your field as in the query. The listener looks to see if it is included and if so does not filter deleted records out.
Doctrine_Core::getTable('Record')->createQuery()->select('id, etc1, etc2')->addSelect('deleted_at')->execute();
will return deleted records.

Categories