Background:
In my application I have an entity that has a self referencing ManyToOne association (many children can point to a single parent). And I have a feature that does mass updates on many entities at one time using the Doctrine ORM. To keep performance from dropping dramatically due to many entities being loaded I detach entities once they've been updated.
Problem:
When I detach an entity that has children and later try to update any of those children Doctrine complains that it doesn't know the parent anymore. Even if I merge the parent entity before trying to update the child.
Question:
What am I doing wrong when I detach the parent entity? I've tried doing cascade="merge" and/or "detach" on the parent column and Doctrine still complains about the parent being an unknown entity when I try to persist.
I've mocked up a simple example that reproduces this. See below.
Test Code:
Entity\Thing.php
/**
* #ORM\Entity()
* #ORM\Table(name="things")
*/
class Thing
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Thing", inversedBy="children", cascade={"detach","merge"})
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent;
/**
* #ORM\OneToMany(targetEntity="Thing", mappedBy="parent")
*/
protected $children;
/**
* #ORM\Column(type="string", length=64)
*/
protected $name;
public function __construct($name = null)
{
$this->children = new ArrayCollection();
$this->name = $name;
}
// .. SNIP ...
}
Test Action:
public function testThingAction($_route)
{
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeThingBundle:Thing');
// simple setup of a couple things in the DB
$t1 = $repo->findByName('Thing1');
if (!$t1) {
$t1 = new Thing('Thing1');
$t2 = new Thing('Thing2');
$t2->setParent($t1);
$em->persist($t1);
$em->persist($t2);
$em->flush();
return $this->redirect($this->generateUrl($_route));
}
list($t1, $t2) = $repo->findAll();
// detach and re-merge Thing1
// This should cause Thing1 to be removed and then re-added
// to the doctrine's known entities; but it doesn't!?
$em->detach($t1);
$em->merge($t1);
// try to update T2
$t2->setName('Thing2 - ' . time());
$em->persist($t2);
// will fail with:
// A new entity was found through the relationship Thing#parent
$em->flush();
return array();
}
The issue is that the child has a relationship to a specific parent object that is no longer managed by Doctrine. When you call $entityManager->merge($entity) you get a new managed entity back from that function.
When you get that back, you need to manually call setParent() on each of your children with the newly managed entity.
Related
I've encountered a problem regarding removing objects, or - to be precise - removing their associations in Symfony2.
Let's assume that we've got 3 entities - Parent, Child and a Grandchild.
For the sake of clarity I'll stick to this erhm, naming.
When removing Parent, I want the association between Parent-Child, and Child-Grandchild to be removed, that is, without any additional dirty workaround; I'd like the ORM layer to do the job.
So, without further ado, here are entities with annotations:
class Parent
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Children", mappedBy="Parent")
*/
private $children;
class Children
/**
* #var Parent
*
* #ORM\ManyToOne(targetEntity="Parent", inversedBy="Children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Grandchildren", mappedBy="Children")
*/
private $Grandchildren;
class GrandChildren
/**
* #var Children
*
* #ORM\ManyToOne(targetEntity="Children", inversedBy="Grandchildren")
* #ORM\JoinColumn(name="children_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $children;
And here is a simple standard deleteAction of a Parent:
public function deleteAction(Request $request, $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$parent = $em->getRepository('AppBundle:Parent')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Parent entity.');
}
$children = $em->getRepository('AppBundle:Children')->findBy(
array(
'parent' => $id,
)
);
$em->remove($parent);
$em->flush();
}
return $this->redirect($this->generateUrl('parent'));
}
With a code as above, after flush, the relationship between Parent-Children disappears (the join column is nulled), but a relation between Children-Grandchildren remains (i.e. the join column children_id isn't reseted to NULL as in Parent-Children situation, the column remains populated with id's).
So, what is the best way to establish this cascading removal? So that when Parent is removed the inherited associations are removed as well. According to the documentation I should do something like: http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-tags-to-be-removed
that is - just iterate through it, preferably encapsulate the process.
But still it doesn't look very clean to me, anyone has a better, cleaner solution?
Any insight would be helpful, cheers.
I tried to implement a versionable ORM using Doctrine2.
Everything works well. When I update an existing entry the old entry gets inserted into the *_version table.
But when I update the entry in another request (and therefor a new instance of EntityManager) the old entry won't be written anymore to the *_version table although the entry in the basic table gets updated without any problems (even the version no. gets incremented by 1).
I like to show you my very simple versionable ORM:
UPDATE: The example code below works now!
Also check my Gist with the logEntityVersion helper method.
ProductBase.php
trait ProductBase
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
// getters and setters here
}
Product.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Product
*
* #ORM\Entity
* #ORM\Table(name="product")
* #ORM\HasLifeCycleCallbacks
*/
class Product
{
use ProductBase;
/**
* #ORM\OneToMany(targetEntity="ProductVersion", mappedBy="product", cascade={"persist"})
*/
private $auditLog;
/**
* #ORM\Column(type="integer")
* #ORM\Version
*/
private $version = 1;
public function __construct()
{
$this->auditLog = new ArrayCollection();
}
public function logVersion()
{
echo sprintf("Creating a new version for ID %s, version %s\n", $this->getId(), $this->getVersion());
$this->auditLog[] = new ProductVersion($this);
}
// Version getter and setter
}
ProductVersion.php
use Doctrine\ORM\Mapping as ORM;
/**
* ProductVersion
*
* #ORM\Entity
* #ORM\Table(name="product_version")
*/
class ProductVersion
{
use ProductBase;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog")
*/
private $product;
/**
* #ORM\Column(type="integer")
*/
private $version;
public function __construct(Product $product)
{
$this->product = $product;
$this->name = $product->getName();
$this->version = $product->getVersion();
var_dump($product->getVersion());
}
// Version getter and setter
}
And this is the code for inserting a new entry and update it to create a new version:
// Insert new product
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$product = new Product();
$product->setName('Product V1');
$this->entityManager->persist($product);
$this->entityManager->flush();
$this->entityManager->commit();
$productId = $product->getId();
echo "Created Product with ID " . $product->getId() . "\n";
/** #var Product $foo */
$foo = $this->entityManager->getRepository('orm\Product')->find($productId);
// Log version (Product V1)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 1
$foo->setName('Product V2');
$this->entityManager->flush();
// Log version (Product V2)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 2
$foo->setName('Product V3');
$this->entityManager->flush();
Schema generation
$tools = new SchemaTool($this->entityManager);
var_dump($tools->getCreateSchemaSql(array(
$this->entityManager->getClassMetadata('orm\Product'),
$this->entityManager->getClassMetadata('orm\ProductVersion')
)));
I see a couple of issues with your code, and unfortunately the concept as well:
Array vs Collection
You've defined Product::$auditLog as an array, but it needs to be a Collection. Use this in stead:
class Product
{
private $auditLog;
public function __construct()
{
$this->auditLog = new \Doctrine\Common\Collections\ArrayCollection();
}
Association mapping
You've defined Product::$auditLog as being mapped by ProductVersion::$product, but haven't defined ProductVersion::$product as being inversed by Product::$auditLog. Fix it like this:
class ProductVersion
{
/** #ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog") */
private $product;
Also, please validate your mappings and database schema. Every error might lead to unexpected results.
$ doctrine orm:validate-schema // plain Doctrine2
$ app/console doctrine:schema:validate // Symfony2
Versioning
You're using the #Version annotation in the trait, meaning both Product and ProductVersion will be versioned by Doctrine. As #Version is used for optimistic locking, I doubt this is what you're really after.
Records in an audit-log should never be updated (or deleted), only added. So locking records doesn't really make sense here.
Please remove ProductBase::$version and in stead add:
Product::$version with #Version annotation.
ProductVersion::$version without #Version annotation.
Now only Product will be versioned by Doctrine, ProductVersion will simply contain the version-number of the product that is logged.
PreUpdate event
You're using #PrePersist and #PreUpdate lifecycle callbacks to populate the audit-log. #PreUpdate is going to be a problem, as the docs state:
Changes to associations of the updated entity are never allowed in this event
Yet you are changing the Product::$auditLog association by adding a record to the collection.
You'll have to move this logic, and there are many options as to where to:
One option could be to start a transaction manually, flush (and keep track of versioned entities), create log entries, flush again, commit the transaction. Or you could replace the second flush with direct inserts using the DBAL connection.
I have a problem with lazy loading in symfony2/doctrine2.
I have a normal object (for example: type item) and this object has an id. If I look at the object at runtime I see that the id is set. Every other parameters like icon and amount are empty. I know, this is how lazy loading works but when I call the getters (getIcon) nothing happens. The icon attribute is still empty. I also tried to call the __load method but it doesn't help.
Sorry, forgot the code
class Character {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
/*********************************************************************
* Custom methods
*/
public function getItem() {
return $this->item;
}
}
And this is the object where the lazy loading not works.
class Item {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="integer")
*/
protected $amount;
/**
* #ORM\Column(type="string")
*/
protected $icon;
}
EDIT2:
Constructor of character class
public function __construct()
{
$this->item = new \Doctrine\Common\Collections\ArrayCollection();
}
So what the previos comments to your initial post are pointing at, is, that you need to implemend a ManyToOne relation in your Item entity to get all your stuff working.
In yout Character Entity you have this lines of code
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
This says you have a relation to an Entity Item which mappes the relation in the attribute "character". In this attribute the relation is stored. If you look into the database, you won't find any stored relations, because you class Item does not have the described mapping attribute character. Like gp_sflover pointed out, a OneToMany relations needs to be Bidirectional an required a ManyToOne relation in the "owning" side. So what you have to do is, add the following code to your Item Entity
/**
* #ORM\ManyToOne(targetEntity="Entity\Character", inversedBy="item")
*/
protected $character;
The inversedBy attribute creates a bidirectional relation. Without this statement, you wouldn't be able to load getItems from your Character entity.
If you have changed your code you have to update your database and to restore the elements. After this, everything will work fine.
/** #Entity */
class First
{
/** #OneToMany(targetEntity="Second", mappedBy="parent") */
protected $secondList;
// access methods here
public function __construct()
{
$this->secondList = new ArrayCollection();
}
}
/** #Entity */
class Second
{
/**
* #ManyToOne(targetEntity="First", inversedBy="secondList")
* #JoinColumn(name="First_id", referencedColumnName="Id")
*/
protected $parent;
}
Here is the problem with taking into ArrayCollection $secondList elements from Second class. Second ManyToOne relation is working properly. Perhaps I did something wrong in initializing a persistance (because First_Id in SQL base is null always).
$first = new Second();
$second = new First();
$first->getSecond()->add($second);
$em->persist($second);
$em->persist($first);
Any suggestions?
Doctrine2 docs say this:
In the case of bi-directional associations you have to update the fields on both sides:
This means that you'll have to do something like:
$second->setParent($first);
As $second table has the foreign key.
Or you could add a cascade={"persist"} option to the$first->secondList property.
You should make sure you close your parenthesis in the First class.
/** #OneToMany(targetEntity = "Second", mappedBy = "parent" ) */
If that is not the problem - is there any error message ?
Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2
The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).
I've used a pretty simple example of a Classroom having many Pupils to keep it simple.
So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.
Lets first define our Doctrine Entities.
The Classroom Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="classroom")
*/
class Classroom
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $miscVars;
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
*/
protected $pupils;
public function __construct()
{
$this->pupils = new ArrayCollection();
}
// ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============
}
The Pupil Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="pupil")
*/
class Pupil
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $moreVars;
/**
* #ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
* #ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
*/
protected $classroom;
// ========== GENERATED FUNCTIONS BELOW ============
}
And our generic Action function:
public function someAction(Request $request, $id)
{
$em = $this->getDoctrine()->getEntityManager();
$classroom = $em->find('AcmeTestBundle:Classroom', $id);
$form = $this->createForm(new ClassroomType(), $classroom);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// Normally you would do the following:
$em->persist($classroom);
$em->flush();
// But how do I create a new row with a new ID
// Including new rows for the Many side of the relationship
// ... other code goes here.
}
}
return $this->render('AcmeTestBundle:Default:index.html.twig');
}
I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.
Thanks in advance to any assistance.
The thing with clone is...
When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.
If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:
public function __clone() {
// Get current collection
$pupils = $this->getPupils();
$this->pupils = new ArrayCollection();
foreach ($pupils as $pupil) {
$clonePupil = clone $pupil;
$this->pupils->add($clonePupil);
$clonePupil->setClassroom($this);
}
}
NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.
When you clone your parent class, this function will create a new collection full of child object clones as well.
$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();
$em->persist($cloneClassroom);
$em->flush();
You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
*/
protected $pupils;
I did it like this and it works fine.
Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.
/**
* Clone element with values
*/
public function __clone(){
// we gonna clone existing element
if($this->id){
// get values (one-to-many)
/** #var \Doctrine\Common\Collections\Collection $values */
$values = $this->getElementValues();
// reset id
$this->id = null;
// reset values
$this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
// if we had values
if(!$values->isEmpty()){
foreach ($values as $value) {
// clone it
$clonedValue = clone $value;
// add to collection
$this->addElementValues($clonedValue);
}
}
}
}
/**
* addElementValues
*
* #param \YourBundle\Entity\ElementValue $elementValue
* #return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
if (!$this->getElementValues()->contains($elementValue))
{
$this->elementValues[] = $elementValue;
$elementValue->setElement($this);
}
return $this;
}
Somewhere just clone it:
// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
I do this:
if ($form->isValid()) {
foreach($classroom->getPupils() as $pupil) {
$pupil->setClassroom($classroom);
}
$em->persist($classroom);
$em->flush();
}