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.
Related
I have these entity
class Centro {
/* ... */
/**
* #ORM\OneToMany(targetEntity=NivelesEscolares::class, mappedBy="centro", cascade={"persist", "remove"}, orphanRemoval=true)
*/
private $nivelesEscolares;
/* ... */
}
Center is linked to a OneToMany relationship with a NivelesEscolares entity. Here is the statement
class NivelesEscolares
{
/*...*/
/**
* #ORM\ManyToOne(targetEntity=App\Entity\Centro::class, inversedBy="nivelesEscolares")
*/
private $centro;
}
When I remove a center, the school level should be removed, but it becomes null instead. I already tried with orphanRemoval but it still doesn't remove anything. Also try with onDelete = "CASCADE" but it doesn't delete anything. I need help with OneToMany relationships.
Well orphanRemoval and onDelete have a little differences as follow:
1. orphanRemoval="true"
the entity on the inverse side is deleted when the owning side entity is AND it is not connected to any other owning side entity anymore. Not exactly, this makes doctrine behave like it is not owned by an other entity, and thus remove it.
implementation in the ORM
2. onDelete="CASCADE"
this will add On Delete Cascade to the foreign key column IN THE DATABASE
This strategy is a bit tricky to get right but can be very powerful and fast. (this is a quote from doctrine official tutorial... but haven't seen much more explaination)
ORM has to do less work (compared to the two previous way of doing) and therefore should have better performance.
if you want to delete the NivelesEscolares when you delete Centro well just use onDelete not orphanRemoval like this:
the Centro should looks like this:
class Centro {
/* ... */
/**
* #ORM\OneToMany(targetEntity=NivelesEscolares::class, mappedBy="centro", cascade={"persist", "remove"})
*/
private $nivelesEscolares;
/* ... */
}
and also use onDelete for NivelesEscolares with #ORM\JoinColumn like this:
class NivelesEscolares
{
/*...*/
/**
* #ORM\ManyToOne(targetEntity=App\Entity\Centro::class, inversedBy="nivelesEscolares")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
private $centro;
}
This will force doctrine to remove the 'school levels' as you said, Enjoy! 🙂
Merge is creating not working for children #OneToMany
I am using Php Doctrine and I am using #OnToMany mapping with cascade all. I have a parent class SalesOrder and child class SalesOrderDetails.
Case 1 : Save - When I save new record sales order along with sales order details. It is working as expected.
Case 2 : Update - Here is the issue, I am merging the Sales Order which is fine however its inserting new records for its children SalesOrderDetail instead of updating it. Ideally it should it apply mergebut for children as well but its not.
As of now, I am getting the Sales Order Details by id from DB then change the properties of it. Ideally that should not be the case, mean if we set the id to unmanned object, it should update instead of creating new records.
Note:
1. Merge is working with parent object if it has the id value.
2. I am not adding new item here, I am just updating the existing recorded through merge.
SalesOrder.php
/**
* #Entity #Table(name="sales_orders")
* */
class SalesOrder extends BaseEntity {
/**
* #OneToMany(targetEntity="SalesOrderDetail",cascade="all", mappedBy="salesOrder" )
*/
protected $itemSet;
function __construct() {
$this->itemSet = new ArrayCollection();
}
}
SalesOrderDetail.php
/**
* #Entity #Table(name="sales_order_details")
* */
class SalesOrderDetail extends BaseEntity {
/** #Id #Column(type="integer") #GeneratedValue * */
protected $id;
/**
* #ManyToOne(targetEntity="SalesOrder")
* #JoinColumn(name="order_no", referencedColumnName="order_no")
*/
protected $salesOrder;
}
Debug Mode screen
If I use cascade={"merge"}
I am getting different error if I am using Cascades merge
Type: Doctrine\ORM\ORMInvalidArgumentException Message: Multiple
non-persisted new entities were found through the given association
graph: * A new entity was found through the relationship
'Ziletech\Database\Entity\SalesOrder#itemSet' that was not configured
to cascade persist operations for entity:
Ziletech\Database\Entity\SalesOrderDetail#0000000052218380000000007058b4a6.
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
'Ziletech\Database\Entity\SalesOrderDetail#__toString()' to get a
clue. * A new entity was found through the relationship
'Ziletech\Database\Entity\SalesOrder#itemSet' that was not configured
to cascade persist operations for entity:
Ziletech\Database\Entity\SalesOrderDetail#0000000052218071000000007058b4a6.
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
'Ziletech\Database\Entity\SalesOrderDetail#__toString()' to get a
clue.
You have a mistake in your mapping, cascade needs an array
/**
* #OneToMany(targetEntity="SalesOrderDetail", cascade={"all"}, mappedBy="salesOrder" )
*/
protected $itemSet;
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.
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.
I've a class user with a one-to-many relationship against ArticleVote which is itself an Association Class (see below).
Here is how my entities looks like:
class User
{
protected $articlesVotes;
}
An user holds an ArticleVote collection.
While an ArticleVote is referenced by a composite primary key based on the UserId and the ArticleId:
class ArticleVote
{
protected $article;
protected $user;
}
Now, let's say I want to remove an ArticleVote from User, naturally I do $user->getArticlesVotes()->removeElement($articleVote); which results in the actual removing of the entity inside the collection but as the ArticleVote is both a relationship and an entity, the row in database is not removed at all.
I know, I can do $em->remove($articleVote); but I wish I could remove it from the collection of the user to bypass the EntityManager, what if I want to remove several $articleVote?
Currently, I create/remove the vote in my User model by passing the Article entity and it's my User entity which creates the ArticleVote object and append it itself, I wish I could have the same behavior for the removal feature.
Any ideas? (oh, and by the way, I already tried with cascade="remove")
I ran to this exact issue yesterday. When setting cascade="remove" this removes the association marked within your UnitOfWork. However to have the items removed from the database you need to mark your $user property in the ArticlesVote Entity to cascade on delete. Like so..
class ArticleVote
{
protected $article;
/**
* #ManyToOne(targetEntity=....
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $user;
}
This will add an "on delete" cascade to the foreign key in your database, and article votes associated with a deleted user will be deleted along with it.