MattVick MattVick - 6 months ago 32
Javascript Question

Symfony SQL error when saving related Doctrine entities using embeded collection of forms

I have been following the How to Embed a Collection of Forms example on the Symfony website.

My situation is a little different. I have 2 Doctrine entities

Experiment
and
Goal
with a composite primary key relationship (see code below).

Basically one
Experiment
can have many
Goals
, but the goal ID is only unique in combination with the
Experiment
ID and
User
ID.

class Goal
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer", options={"default": 1})
* @ORM\Id
*/
protected $id = 1;

/**
* @var integer
*
* @ORM\Column(name="experiment_id", type="integer", nullable=true, options={"default": null})
* @ORM\Id
*/
protected $experimentId;

/**
* @ORM\ManyToOne(targetEntity="Experiment", inversedBy="goals")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="experiment_id", referencedColumnName="id"),
* @ORM\JoinColumn(name="user_id", referencedColumnName="user_id")
* })
*/
protected $experiment;

/**
* @var integer
*
* @ORM\Column(name="user_id", type="integer", nullable=true, options={"default": null})
* @ORM\Id
*/
protected $userId;

/**
* @ORM\ManyToOne(targetEntity="Optimcore\UserBundle\Entity\User", inversedBy="goals", cascade={"persist"})
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;

// ...
}


I have an
ExperimentType
form that embeds a
'collection'
of
GoalType
forms.

in my controller I have the following code:

$originalGoals = new ArrayCollection();
foreach ($experiment->getGoals() as $goal) {
$originalGoals->add($goal);
}

$form = $this->createForm(new ExperimentType(), $experiment, array(
'action' => $this->generateUrl('experiment_goals', array('id' => $experiment->getId())),
'method' => 'PUT',
));
$form->handleRequest($request);

if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();

// remove the relationship between the Goal and the Experiment
foreach ($originalGoals as $goal) {
if (false === $experiment->getGoals()->contains($goal)) {
$goal->setExperiment(null);
$em->persist($goal);
$em->remove($goal);
}
}

foreach ($experiment->getGoals() as $goal) {
if (is_null($goal->getUserId())) {
$this->getUser()->addGoal($goal);
$goal->setUserId($this->getUser()->getId());
$goal->setUser($this->getUser());
$em->persist($goal);
}
}

$em->flush();

}


I have javascript to add and remove goal forms on the page. New goals are assigned the next available ID, so if the last goal on the page has the ID 2 the next goal is given the ID 3.

Adding new goals and saving to the DB works fine, deleting goals and saving also works fine.

However when a goal is deleted, by the javascript on the page, and then a new goal is added by javascript this goal is assigned the same ID as the goal that was deleted. Now when the form is submitted and Doctrine tries to save the Goals an SQL error is thrown because Doctrine tries to INSERT a Goal with the same ID as the one deleted by the Javascript but still in the database.

How do I go about getting Doctrine to delete the first Goal before inserting the new one? I've tried flushing earlier but this doesn't help. How do I force Doctrine to only delete the old Goal before adding saving the new one? or am I going about this all wrong?

Here's the error message:

An exception occurred while executing 'INSERT INTO goal (id, experiment_id, user_id, name) VALUES (?, ?, ?, ?)' with params ["2", 183, 6, "Goal 2"]:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '2-183-6' for key 'PRIMARY'

Answer

A very simple answer to a very long question was found in the Doctrine documentation.

10.2. How Doctrine Detects Changes

  • Flush only a single entity with $entityManager->flush($entity).

To get Doctrine to delete the original Goal before saving the new one I have to add $em->flush($goal); after $em->remove($goal);. This will flush only that single entity, deleting the record from the database.

foreach ($originalGoals as $goal) {
    if (false === $experiment->getGoals()->contains($goal)) {
        $goal->setExperiment(null);
        $em->persist($goal);
        $em->remove($goal);

        // commit this change to the database
        $em->flush($goal);
    }
}
Comments