I have an entity Template and another one Request. Essentially, a template represents an html form, and a request will represent a collection of values which the form was filled with and a reference to the template id.
class Request {
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/**
* #ManyToOne(targetEntity="Template", cascade={"persist"})
* #JoinColumn(name="templateId", referencedColumnName="id", nullable=false)
*/
private $template;
...
What I am trying to achieve is that when Request is loaded from the DB then the object comes holding the relevant Template object with all its data. However, when requests are saved there is no need to save the template too... thus cascade={"persist"} should not be there.
1- Load all templates from db
2- User selects a template from a dropdown
3- Tmeplate shows on screen and the user fills it in
4- Request is saved
$request = new \entities\Request();
//template already exist in the db
$template = $this->templateRepository->fetchTemplate(1);
$request->template = $template;
...
$this->entityManager->persist($request);
$this->entityManager->flush();
Now the problem is when I use casade persist it saves another Template in the templates table. If I do not use cascade persist it errors:
Fatal error: Uncaught exception 'Doctrine\ORM\ORMInvalidArgumentException' with message 'A new entity was found through the relationship 'entities\Request#template' that was not configured to cascade persist operations for entity: entities\Template#00000000343e07770000000073e3b0ec. 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 'entities\Template#__toString()' to get a clue.' in C:\Development\wamp\www\vendor\doctrine\orm\lib\Doctrine\ORM\ORMInvalidArgumentException.php on line 59
What is the correct Doctrine relationship setting to achieve the desired behaviour?
Related
I am getting the following json data from the client. JSON contains the parent and child details as below -
{
id : 1,
name : "Parent"
children : [
{ id : 1, name : "A" },
{ id : 2, name : "B" }
]
}
I am mapping these json data to Parent and Child Object.
Parent
class Parent{
/** #Id #Column(type="integer",name="order_no") #GeneratedValue * */
protected $id;
/**
* #OneToMany(targetEntity="Child",cascade={"merge"}, mappedBy="parent" )
*/
protected $children;
}
Child
class SalesOrderDetail extends BaseEntity {
/** #Id #Column(type="integer") #GeneratedValue * */
protected $id;
/**
* #ManyToOne(targetEntity="parent")
* #JoinColumn(name="parent_id")
*/
protected $salesOrder;
}
So far so good.
Now the issue is when I am trying to merge the parent
$em->merge($parent)
I am getting the following error. Note : The entire object parent and children are unmanaged object so I am trying to merge. If I just merge parent it works find but getting error if I am trying to save entire content of parent and its children.
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\Parent#itemSet' that was not configured to
cascade persist operations for entity:
Ziletech\Database\Entity\Child#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\Child#__toString()' to get a clue. * A new
entity was found through the relationship
'Ziletech\Database\Entity\Parent#itemSet' that was not configured to
cascade persist operations for entity:
Ziletech\Database\Entity\Child#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\Child#__toString()' to get a clue.
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.
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.
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.