Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cloning an entity in Symfony2 saves changes to original record and cloned record when persisted via Doctrine

I have a form that allows me to save a record or duplicate it. The form saves the record as a $view entity, which happens to have multiple associated entities, e.g. $viewVersion that are managed by the form builder in a formType with nested entities (this probably is irrelevant).

If I make changes and submit the form to "duplicate", the code clones the $view object with a function on my entity that unsets the $view->id and other associations. This forces Doctrine to make a new record when it persists the record to the database. This works perfectly. Hurray!

BUT, the changes made to the record are ALSO persisted to the original entity that was cloned (and consequently saved to the database). So it is saving these changes to TWO database records. I happen to like this functionality, but I need to understand WHY it's doing it so it doesn't break later. Here are the relevant bits of code in summary:

// File: CmsBundle/Controller/AdminEditController.php

// Get the Entity Manager
$em = $this->getDoctrine()->getManager();

// Get the View based on the requested ID
// Is there some magic that happens here to make the entity manager track this $view entity?
$view = $em->getRepository("GutensiteCmsBundle:View\View")->find($request->query->get('id'));

// Various bits of code to do whatever I want before a save
// ...

if ($request->isMethod( 'POST' )) {
    $form->handleRequest($request);
    if( $form->isValid() ) {
        // Duplicate the view entity if the view button is pushed
        if(
            $form->has('duplicate') 
            && $form->get('duplicate')->isClicked()
        ) {
            $view = clone $view;
        }

        // Persist the cloned view
        $em->persist($view);
        $em->flush();
    }
}

The View entity has a special clone function that gets triggered on a clone, which resets the ids of the cloned versions:

// File: CmsBundle/Entity/View.php

public function __clone() {
    if($this->id) {
    $this->setId(null);
    $this->setLockVersion(1);
    $this->setPublished(null);

    // Clone associated entities and reassociate with THIS version (even though there is no id yet, there will be when it persists)
    // clone the current version (which also has a clone function like this)
    $version = clone $this->getVersion();
    // reset the viewid with a custom function
    $version->resetView();
    // Add this cloned verion to the version history
    $this->addVersion($version);
}

I've read a lot about cloning, and consistently I'm told that you don't need to detach the original $view from the entity manager. Besides, I've tried, and it didn't do any good. The changes to $view, which were submitted by the form and processed to $view prior to the cloning, are still saved to the original $view record id (e.g. 33), as well as to the new cloned record (e.g. 62). So two persists are happening, even though, only one persist is called on a single entity.

What is going on?

Update

I am told that if you load an entity with the entity manager, it is being tracked by the entity manager. So if you call flush() at any time, any changes will be persisted, even if you did not call persist($view) on the entity. So when I clone the entity, the entity manager is effectively managing 2 entities: the original and the clone.

I've tried detaching the view from the entity manager before the clone in two ways:

// detach method 1
$em->detach($view); 
$em->flush();

// detach method 2
$em->refresh($view); 
$em->flush();

// clone the view after detaching the first entity.
$view = clone $view;

But the entity manager still persists the changes to the original $view record.

I also tried the suggestion to add unset($this->_entityPersister, $this->_identifier); to my custom __clone() method. But that also did not detach the the original entity or the cloned version from the entity manager. The changes were saved to both the old record and the new record.

Nothing seems to make the entity manager ignore the original entity.

Useful References

  • How Clone Works
  • Implement Your Own Clone
  • Clone Nested
like image 404
Chadwick Meyer Avatar asked Jul 12 '14 01:07

Chadwick Meyer


1 Answers

Persist is only needed when you attaching something to your Entity Manager . But in your case original "$view record id (e.g. 33)" already within it. So basically, what happens:

$view1 = new View();
$view1->text = '1';
$em->persist($view1);
$em->flush();

Now your have one record with text == '1' stored. Then:

$view1->text = 'one'; //important!

$view2 = new View();
$view2->text = 'two';

$view3 = new View();
$view3->text = 'three';

$em->persist($view2);
$em->flush();

Call of flush() updates your $view1, inserts your $view2, and ignores your $view3 since last is not persisted. As a result you have two records 'one' and 'two'.

It is possible to call flush() for selected objects. So call of $em->flush($view2) will only insert $view2 and leave $view1 untouched.

In your simple example it will work.

But make sure $em->flush() won't happen any further.

Otherwise to be sure that your $view1 will stay unchanged try to $em->refresh($view1) it.

like image 68
Dropaq Avatar answered Nov 09 '22 21:11

Dropaq