0

I am trying to implement a "Clone" action on a page listing entities ("Circuits"). The workflow is the following:

  1. user selects an entity then selects the "Clone" action in a dropdown
  2. the CircuitsController::indexAction method is fired, redirects the user to the CircuitsController::editAction page and pass along the Circuit's ID and a GET parameter $is_clone = true;
  3. user modifies whatever he wants, then submits
  4. the CircuitsController::editAction method sees that a clone request has been made, clones the Circuit entity, persist it then flush the EntityManager.

My issue here is that when the EntityManager::flush method is called, two strictly identical INSERT queries are performed, thus leading to a rollback and no new circuit persisted.

Here is the portion of code from the CircuitsController::indexAction that redirects:

case 'duplicate':
    $this->denyAccessUnlessGranted('ROLE_NETWORK', null, 'Insufficient rights to perform selected action.');
    if (count($formData['circuits']) != 1) {
        throw new \RuntimeException('Cannot duplicate more than one item.');
    }
    /* 'circuits' key points to an array of circuits */
    $circuit = $formData['circuits'][0];
    return $this->redirect($this->generateUrl('net_dev_web_circuits_edit',
                                              ['id' => $circuit->getId(),
                                               'type' => $type,
                                               'clone' => true]));

My CircuitsController::editAction:

public function editAction(Request $request, Circuit $circuit, $type = 'mnms') {
    $this->denyAccessUnlessGranted('ROLE_NETWORK', null, 'Insufficient rights to perform selected action.');

    $em = $this->getDoctrine()->getManager();
    $clone = $request->query->get('clone');
    $circuitForm = $this->buildCircuitsForm($circuit, $type, ['is_clone' => $clone]);

    if (empty($clone)) {
        $lcmForm = $this->generateLcmForm($em, $circuit);
    }
    else {
        $lcmForm = null;
    }

    if ($request->isMethod('POST')) {
        if ($circuitForm->handleRequest($request)->isValid()) {
            if ($clone) {
                $em->detach($circuit);
                $circuit = clone $circuit;
                $em->persist($circuit);

                $endPointRepo = $em->getRepository('NetDevCoreBundle:EndPoint');
                $mnms = $circuit->getMnmsCircuit();
                $endPoints = $endPointRepo->findByCircuit($mnms);

                foreach ($endPoints as $endPoint) {
                    $device = $endPoint->getDevice(); /* @var $device Device */
                    $defaultEndPoint = (new EndPoint())
                                     ->setCircuit($circuit)
                                     ->setDevice($device)
                                     ->setSite($endPoint->getSite())
                                     ;
                    $device->addCircuit($circuit);
                    $em->persist($defaultEndPoint);
                }

            }

            $em->flush();
            $request->getSession()->getFlashBag()->add('notice', 'Circuit saved');
            return $this->redirect($this->generateUrl('net_dev_web_circuits_edit', array('type' => $type,
                                                                                         'id' => $circuit->getId())));
        }

        /* [.. unrelated code (I hope) to get the edit form ..] */
    }

Following is the __clone method of my entity:

public function __clone() {
    if ($this->id) {
        $this->id = null;
        $this->listenPops = new ArrayCollection();
        $this->endPoints = new ArrayCollection();
    }
}

And finally, the two issued SQL statements:

#20 0.29 ms:
"START TRANSACTION"
Parameters: [null] 

#21 0.29 ms:
INSERT INTO Circuit (
  two_way, circuit_name, circuit_type, 
  mtu, port_bindings, user_enable, 
  bandwidth, tierLevel, translate, 
  crossconnect, header_compression, 
  padding_removal, untag_enable, landside_tx_duplicate_enable, 
  landside_tx_duplicate_vlan_id, 
  priority, trunk_force_mask, connection_number, 
  aptUserVlanIds, user_max_age, validationDate, 
  synchronizationDate, updatedAt, 
  macAddresses, vlan_id, mnms_circuit, 
  parent_route, customer
) 
VALUES 
  (
    ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 
    ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
  )
Parameters: { 1: 1, 2: MNMS_Tier1_ZZZ_XX_3171, 3: mnms, 4: 23, 5: 1, 6: 1, 7: 100, 8: 0, 9: 0, 10: '', 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 1, 17: 0, 18: 1, 19: 'a:4:{i:1;N;i:2;N;i:3;N;i:4;N;}', 20: null, 21: '2015-08-19 11:33:42', 22: null, 23: '2015-09-07 18:48:37', 24: 'a:0:{}', 25: 233, 26: null, 27: 13, 28: 64 } 

#22 0.00 ms:
INSERT INTO Circuit (
  two_way, circuit_name, circuit_type, 
  mtu, port_bindings, user_enable, 
  bandwidth, tierLevel, translate, 
  crossconnect, header_compression, 
  padding_removal, untag_enable, landside_tx_duplicate_enable, 
  landside_tx_duplicate_vlan_id, 
  priority, trunk_force_mask, connection_number, 
  aptUserVlanIds, user_max_age, validationDate, 
  synchronizationDate, updatedAt, 
  macAddresses, vlan_id, mnms_circuit, 
  parent_route, customer
) 
VALUES 
  (
    ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 
    ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
  )
Parameters: { 1: 1, 2: MNMS_Tier1_ZZZ_XX_3171, 3: mnms, 4: 23, 5: 1, 6: 1, 7: 100, 8: 0, 9: 0, 10: '', 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 1, 17: 0, 18: 1, 19: 'a:4:{i:1;N;i:2;N;i:3;N;i:4;N;}', 20: null, 21: '2015-08-19 11:33:42', 22: null, 23: '2015-09-07 18:48:37', 24: 'a:0:{}', 25: 233, 26: null, 27: 13, 28: 64 } 

#23 3.96 ms 
"ROLLBACK"
Parameters: [null] 

I have been fiddling around with that but to no avail. I am wondering if this would not be caused by Doctrine trying to persist the newly cloned Entity AND the one used as a template but I have no idea how I could (dis)prove that.

Any help would be greatly appreciated.

EDIT 1:

Thanks to @user2268997's comment, I now know that the second INSERT refers to the "template" circuit. Doctrine tries to update it as well, which is not what I want (I need the original data to be left unchanged). Any way I can tell Doctrine not to update the original entity ?

Community
  • 1
  • 1
NaeiKinDus
  • 730
  • 20
  • 30
  • Probably that is your case: http://stackoverflow.com/questions/9071094/how-to-re-save-the-entity-as-another-row-in-doctrine-2 – Alex Sep 08 '15 at 11:20
  • put another flush after `$em->persist($circuit)` and see what happens. – user2268997 Sep 08 '15 at 11:36
  • @Alex I've used that answer to create the code in the first place :-) I've tried to swap the detach and the clone, but the issue is still here and identical :-/ – NaeiKinDus Sep 08 '15 at 11:54
  • @user2268997 I've added a flush. There is now only one INSERT (the newly cloned circuit), but the second INSERT is replaced by an UPDATE on the canditate-to-be-cloned circuit. It seems that the second INSERT was indeed linked to the template circuit. In which case, how can I tell Doctrine / Symfony not to try to update it ? – NaeiKinDus Sep 08 '15 at 11:58
  • What's the update trying to do, i.e: what's changing? – user2268997 Sep 08 '15 at 12:21
  • @user2268997 it was trying to add a relationship that cannot exist (duplicate created by the data used in the cloned entity). But I think I've figured out how to make this work, thanks to you :) – NaeiKinDus Sep 08 '15 at 12:27

1 Answers1

2

Since I was able to figure out that the former entity was causing the duplicate insert (thanks to @user2268997), I've looked into a way to tell Doctrine not to perform anything on it. Here's the resulting code:

if ($clone) {
    $clonedCircuit = clone $circuit;

    /* Here's the "fix" */
    $em->refresh($circuit);

    $em->detach($clonedCircuit);
    $em->persist($clonedCircuit);
    $em->flush();
/* [...] */
}
$circuit = $clonedCircuit;

The Circuit is correctly cloned and persisted, and relations with other entities seem correct as well.

NaeiKinDus
  • 730
  • 20
  • 30