Flush a single entity doesn't update relation - php

Passing an entity to the flush() method allow Doctrine to only update this entity, which is great for optimization. But it seems that the relations are not updated when I'm doing this.
Example :
$event->getEmails()->first()->setEmail('mynewemail#email.com');
$em->flush($event); // Emails wont be updated
$em->flush(); // Emails will be updated
The mapping:
class Event
{
/**
* #var ArrayCollection|Email[]
*
* #ORM\OneToMany(targetEntity="Email", mappedBy="event", cascade={"all"}, orphanRemoval=true)
* #ORM\OrderBy({"id"="asc"})
*/
protected $emails;
I checked inside Doctrine code, and here is what I found : internally, when I flush a single entity, the method computeSingleEntityChangeSet is called. The comment above this method is the following:
/**
* Only flushes the given entity according to a ruleset that keeps the UoW consistent.
*
* 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
* 2. Read Only entities are skipped.
* 3. Proxies are skipped.
* 4. Only if entity is properly managed.
* ...
*/
According to first rule, changes in collections are processed as well. So am I doing something wrong, or is this a bug of Doctrine?

With $event->getEmails()->first()->setEmail('mynewemail#email.com'); you're not updating the collection, but one Entity in the collection. It's normal that the single entity flush does not update the Email entity.
If you do write $event->addEmail($aNewEmailEntity); (same with remove), then you'll see that the collection is indeed updated when calling the single entity flush.

Related

Issue with Doctrine OneToMany relationship

I have an issue with the doctrine relationship. I try different ways but anything won't work.
Idea is that I have a News entity and every news should have many comments. So I try next:
The News entity:
/**
* #ORM\OneToMany(targetEntity="App\ORM\Entity\NewsComment", mappedBy="news")
*/
protected \Doctrine\Common\Collections\Collection $comments;
/**
* News constructor.
*/
public function __construct() {
$this->comments = new ArrayCollection();
}
And NewsComment entity:
/**
* #ORM\ManyToOne(targetEntity="App\ORM\Entity\News", inversedBy="comments")
*/
protected \App\ORM\Entity\News $news;
Every entity has its own get and set methods as well.
But, when I receive a News entity a can get comments collection but it always empty. On the other hand, I can take any NewsComment entity and get from this News entity. It is working fine. But not to another way.
Is anything wrong with my code?
Doctrine sets owned (non-inversed) collection as lazy by default.
When retrieving an entity by database, you should see an empty PersistentCollection instead of ArrayCollection, with initialized property set to false.
When calling any method on that collection, doctrine fires the queries needed to initialize the collection and populate it.
Collection emptiness should be only checked invoking isEmpty.

Doctrine launches INSERT always instead of UPDATE for existing entities

Let's say I have the following entities:
App\Entity\MainEntity:
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType1FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType1;
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType2FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType2;
Basically, one-directional 1:1 relationship from main entity to the same dependent entity using two different columns in the main entity table.
It doesn't matter, whether I use fetch="EAGER" or normal lazy loading through Doctrine proxy classes, when I do something like this:
$mainEntity = $this->mainEntityRepository->find(74);
$mainEntity->setDependentEntityType1($this->dependentEntityRepository->find(35));
$this->mainEntityRepository->saveTest($mainEntity);
where ::saveTest() is:
public function saveTest(MainEntity $mainEntity) {
$this->_em->persist($mainEntity->getDependentEntityType1());
$this->_em->merge($mainEntity);
$this->_em->flush();
}
it always tries to INSERT a new dependent entity to the table, even though I never made any changes (and even if I made them, it should have been UPDATE! for it)
The question is: why does Doctrine decide this dependent entity is a new one if I did $this->dependentEntityRepository->find(35) , so loaded an existing one?
I tried fetch="EAGER" thinking that spl_object_hash might return different hashes for a Proxy class instance and the actual DependantEntity one, but it doesn't matter, the DependantEntity is for some reason always considered as "new".
UPDATE: here is the code of ::setDependentEntityType1()
public function setDependentEntityType1(DependentEntity $dependentEntity) : void {
$this->dependentEntity = $dependentEntity;
}

Symfony 3 / Doctrine - Get changes to associations in entity change set

So I already know that I can get changes to a specific entity in the preUpdate lifecycle event:
/**
* Captures pre-update events.
* #param PreUpdateEventArgs $args
*/
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof ParentEntity) {
$changes = $args->getEntityChangeSet();
}
}
However, is there a way to also get changes for any associated Entities? For example, say ParentEntity has a relationship setup like so:
/**
* #ORM\OneToMany(targetEntity="ChildEntity", mappedBy="parentEntity", cascade={"persist", "remove"})
*/
private $childEntities;
And ChildEntity also has:
/**
* #ORM\OneToMany(targetEntity="GrandChildEntity", mappedBy="childEntity", cascade={"persist", "remove"})
*/
private $grandChildEntities;
Is there a way to get all relevant changes during the preUpdate of ParentEntity?
All of the associated entities from a OneToMany or ManyToMany relationships appear as a Doctrine\ORM\PersistentCollection.
Take a look at the PersistentCollection's API, it have some interesting public methods even if they are marked as INTERNAL: https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/PersistentCollection.php#L308
For example you can check if your collection is dirty which means that its state needs to be synchronized with the database. Then you can retrieve the entities that have been removed from the collection or inserted into it.
if ($entity->getChildEntities()->isDirty()) {
$removed = $entity->getChildEntities()->getDeleteDiff();
$inserted = $entity->getChildEntities()->getInsertDiff();
}
Also you can get a snapshot of the collection at the moment it was fetched from the database: $entity->getChildEntities()->getSnapshot();, this is used to create the diffs above.
May be this is not optimal, but it can do the job. You can add a version field on ParentEntiy with a timestamp, then on each related entity setter function (Child or GranChild) you need to add a line updating that parent timestamp entity. In this way each time you call a setter you will produce a change on the parent entity that you can capture at the listener.
I have used this solution to update ElasticSearch documents that need to be updated when a change happens on a child entity and it works fine.

Doctrine2 - #OneToOne unidirectional entity returns cascade persistence error

I have the following class:
/**
* #ORM\Entity(repositoryClass="Repository")
* #ORM\Table(name="my_data")
* #ORM\HasLifecycleCallbacks
*/
class Data extends ModelEntity
{
/**
* #var integer $id
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var Customer $userId
*
* #ORM\OneToOne(targetEntity="\Shopware\Models\Customer\Customer")
* #ORM\JoinColumn(name="userId", referencedColumnName="id")
*/
private $userId;
//in the class are more methods + getters and setters #not important
}
The Customer Entity is not a class of mine, therefore I want this relationship to be unidirectional and don't care how the Customer looks like (minus the id field).
Now from Doctrine documentation I got that I don't need any other cascade={"persist"} thingy like in the error I get:
exception 'Doctrine\ORM\ORMInvalidArgumentException' with message
'A new entity was found through the relationship
'Data#userId' that was not configured to cascade persist operations
for entity:
Shopware\Models\Customer\Customer#0000000035d66eab000000001b1ed901.
To solve this issue: Either explicitly call EntityManager#persist()
on this unknown entity or configure cascade persist
this association in the mapping for example
#ManyToOne(..,cascade={"persist"}).
If you cannot find out which entity causes the problem implement
'Shopware\Models\Customer\Customer#__toString()' to get a clue.'
what am I doing wrong? And how can I fix this error?
PS: I don't want to modify or save any information in this Customer, just to have a reference to the entity.
LE: Due this info Doctrine documentation: cascade-persist I will output also some code from where the error is triggered. As it says in the documentation:
New entities in a collection not marked as cascade: persist will produce an Exception and rollback the flush() operation.
the error is trown by the following flush()
$this->entityModel->persist($this->getDataInstance());
$this->entityModel->flush();
and the getDataInstance method is Data entity that has a Customer, the Customer Object is found from its repository through method findBy('Customer', id). The Customer entity it is never new instatiated or modified, therefore no need to persist it.
The getDataInstance:
protected function getDataInstance()
{
if (is_null($this->data)) {
$this->initData();
}
return $this->data;
}
and the initData:
private function initData()
{
if (is_null($this->data)) {
$this->data = new Data();
/**
* #var \Shopware\Models\Customer\Customer $customer
*/
$customer = $this->entityModel->getRepository('Shopware\Models\Customer\Customer')->findOneBy(['id' => $this->session->sUserId]);
$this->data->setUserId($customer);
}
}
UPDATE: Still didn't find an answer, but I found some leads after speaking with someone on the doctrine #IRC channel.
Here is the conversation:
p1: did your Customer entity maybe get detached? for instance de/serialization, using more than one entity manager or calling $em->clear() can lead to detaching
p1: do you expect to create new customer? if no then either you did, or it "found" an already existing entity which would suggest one of the above
me: $em->clear() is not called by me for sure
me: the de/serialization I'm not sure ... the shop has a HTTP-cache that can do that
p1: where do you get the entity from? yes, cache might do it if it caches customers without knowing about orm
me: and I don't expect to create a new customer, I just called from the repository one
me: $customer = $this->swagModelManager->getRepository('Shopware\Models\Customer\Customer')->findOneBy(['id' => $this->session->sUserId]);
me: which retruns either null or the object
p1: is $this, respectively $this->data cached between requests possibly?
me: the method findOneBy can be find in the EntityRepository.php (line 192)
me: hmm I don't know, the thing is on my local host and other installation that never happens, but on 1 client server it does :D and I assumed that has to do with the caches and his settings on the server
me: so if that's the problem how can I fix it ?
me: to don't call the cascade persist
p1: the detached entity can be reattached by doing "$entity = $em->refresh($entity)" or "$entity = $em->merge($entity)" (they differ in handling of possible changes between the entity state and the db - refresh reloads the data from db - "resets", merge updates the db) - but that is not a fix if you do not know why it happens - you probably can't tell when it is safe to do it
me: I know, it is bad that I cannot reproduce that - thanks for the tips!
p1: the main thing - if EM says it found "new" entity and it already exists in the DB, it is not "managed" by that EM - either it has been detached somehow or it is still attached but to a different EM instance
me: then an $em->refresh($entity) before the persist should sufice, right? because I don't want to change anything related to this Entity
p1: you would have to do that to the referenced customer, so "$entity->customer = $em->refresh($entity->customer)" - but beware of possible side effects of reassigning that (any events, side effects in getter or similar stuff if you have it)
Due to the fact that I cannot reporduce the error right now, I cannot say if the end of the conversation is the right answer or not. When I will know for sure I will revise the question/answer.

Doctrine 2 Entities Relations Remove

I have an owning entity that has the following relation to an "attribute" entity:
/**
* #ORM\OneToMany(targetEntity="Attribute", mappedBy="entity", cascade={"persist", "remove", "merge"})
**/
protected $attributes;
On the side, the owned entity relation looks like this:
/**
* #ORM\ManyToOne(targetEntity="Entity", inversedBy="attributes")
* #ORM\JoinColumn(name="entity_id", referencedColumnName="id")
*/
protected $entity;
When I create an instance of an entity, add attributes to it and save it. It all works fine.
When I remove one attribute from the entity and persist, the attribute is not deleted in the database and re-appears upon refresh.
Anyone has an idea?
Solution
What you're looking for is orphan removal.
You can read on if you'd like details on why your current situation isn't working.
Cascade woes
The cascade operation won't do what you want unfortunately. The "cascade=[remove]" just means that if the entity object is removed then doctrine will loop through and remove all child attributes as well:
$em->remove($entity);
// doctrine will basically do the following automatically
foreach ($entity->getAttributes() as $attr)
{
$em->remove($attr);
}
How to do it manually
If you needed to remove an attribute from an entity you'd delete the attribute like so:
$entity->getAttributes()->removeElement($attr);
$em->remove($attribute);
Solution details
But, to do that automatically we use the orphan removal option. We simply tell doctrine that attributes can only belong to entities, and if an attribute no longer belongs to an entity, simply delete it:
/**
* #ORM\OneToMany(targetEntity="Attribute", mappedBy="entity", orphanRemoval=true, cascade={"persist", "remove", "merge"})
**/
protected $attributes;
Then, you can remove the attribute by simply doing this:
$entity->getAttributes()->removeElement($attr);
Be careful when using orphan removal.
If you remove an element and then call refresh on the main entity the element is not removed from the internal orphan removal array of doctrine.
And if flush is called later, will result in removing that entry from the db, ignoring the refresh.
This looks like a bug to me, and resulted in loss of images on a lot of products in my app. I had to implement a listener to call persist again on those entities, after they ware scheduled for delete.

Categories