Symfony2 form and Doctrine2 - update foreign key in assigned entities fails

12,294

Solution 1

If i understand you correctly your relation is bidirectional, so you have to specify an owning side and an inverse side. As Doctrine 2 documentation:

  • The inverse side has to use the mappedBy attribute of the OneToOne, OneToMany, or ManyToMany mapping declaration
  • The owning side has to use the inversedBy attribute of the OneToOne, ManyToOne, or ManyToMany mapping declaration
  • ManyToOne is always the owning side and OneToMany is always the inverse side

So i messed up with your association in my first answer. In your case Profile has to be the inverse side while study should be the owning side. But you are working on Profile, so you need the cascade annotation on profile to persist new entities:

class Profile
{
    /**
     * Bidirectional (INVERSE SIDE)
     *
     * @ORM\OneToMany(targetEntity="Study", mappedBy="profile",
     *     cascade={"persist", "remove"})
     */
    private $studies;
}

class Study
{
    /**
     * Bidirectional (OWNING SIDE - FK)
     * 
     * @ORM\ManyToOne(targetEntity="Profile", inversedBy="studies")
     * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
     */
    private $profile;
}

Note that your example is exactly the same as that on Doctrine2 documentation.

Solution 2

You have to set profile to instance. In controller. You havent pasted controller code.. You should iterate over $profile->getStudy collection and set for each item ->setProfile=$profile.

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html#use-case-1-dynamic-attributes

What is happening there doesnt apply to forms..He actually has a this reference..Hm maybe adding this is a kind of auto fix to avoid iteration, not sure whether it works, i'll try

Share:
12,294
koral
Author by

koral

Updated on June 28, 2022

Comments

  • koral
    koral almost 2 years

    I have profiles and studies. One person can finish many studies. The form renders correctly. There is a button "Add new study" and with jQuery I add another subform based on data-prototype and this works well. When I submit such a form with new subforms I get an database error

    Integrity constraint violation: 1048 Column 'profile_id' cannot be null 
    

    I understand this error but I don't know how to get rid of it. I know I can update collection of studies after binding in controller but I hope there is a way to configure it properly in annotations. When I only update entities everything works fine.

    The code is:

    class Profile {
        /**
         * @var integer $profileId
         *
         * @ORM\Column(name="profile_id", type="integer", nullable=false)
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="IDENTITY")
         */
        private $profileId;
    ...
        /**
         *
         * @var Study
         * @ORM\OneToMany(targetEntity="Study", mappedBy="profile", cascade={"persist", "remove"})
         */
        private $study;
    
        public function __construct()
        {
            $this->study = new \Doctrine\Common\Collections\ArrayCollection();
        }
    ...
    }
    

    and

        class Study {
        /**
         * @var integer $studyId
         *
         * @ORM\Column(name="study_id", type="integer", nullable=false)
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="IDENTITY")
         */
        private $studyId;
    ...
        /**
         * @var Profile
         *
         * @ORM\ManyToOne(targetEntity="Profile")
         * @ORM\JoinColumns({
         *   @ORM\JoinColumn(name="profile_id", referencedColumnName="profile_id")
         * })
         */
        private $profile;
    ...
    }
    

    with s(g)etters. Underlaying database structure is

    CREATE TABLE IF NOT EXISTS `profile` (
      `profile_id` int(11) NOT NULL AUTO_INCREMENT,
      PRIMARY KEY (`profile_id`),
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
    
    CREATE TABLE IF NOT EXISTS `study` (
      `study_id` int(11) NOT NULL AUTO_INCREMENT,
      `profile_id` int(11) NOT NULL,
      PRIMARY KEY (`study_id`),
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
    
    ALTER TABLE `study`
      ADD CONSTRAINT `study_fk2` FOREIGN KEY (`profile_id`) REFERENCES `profile` (`profile_id`);
    

    Form buidlers are:

    class ProfileType extends AbstractType {
    
        public function buildForm(FormBuilder $builder, array $options)
        {
            $builder->add('study', 'collection', array(
                        'type' => new StudyType(),
                        'allow_add' => true,
                        'allow_delete' => true
                            )
                    )
        }
    ...
    }
    

    and

    class StudyType extends AbstractType {
    
        public function buildForm(FormBuilder $builder, array $options)
        {
            $builder
                    ->add('city') //example field not included in above structures
        }
    ...
    }
    

    The Javascript part

    function profileNewStudy() {
        var nr = $('[class^=profile_study_][class*=type2]').last().parent();
        nr = nr.attr('id').split('_');
        nr = nr.pop()*1 + 1;
        var proto = $('#profile_study').data('prototype');
        proto = proto.replace(/\$\$name\$\$/g, nr);
        $('#profile_study').append(proto).find('.profile_study_' + nr + ' input').val('qpa' + nr);
    }
    

    and Twig template

    <form action="#" method="post" {{ form_enctype(form) }}>
        {{ form_widget(form) }}
        <input type="submit" value="Zapisz"/>
    </form>
    

    For testing purposes I removed from database constraint NOT NULL on study.profile_id and then everything was saved except that study.profile_id=null.

    edited after @Gremo answer

    I did some tests. Unfortunately it didn't help :( I corrected his mistakes with code

    class Profile
        /**
         * @var Study
         * @ORM\OneToMany(targetEntity="Study", mappedBy="profile")
         */
        private $study;
    class Study
        /**
         * @var Profile
         * @ORM\ManyToOne(targetEntity="Profile", inversedBy="study")
         * @ORM\JoinColumn(nullable=false, onDelete="CASCADE", referencedColumnName="profile_id")
         */
        private $profile;
    

    and when I added new Study entity in form and posted it to server I got an error: A new entity was found through the relationship 'Alden\xxxBundle\Entity\Profile#study' that was not configured to cascade persist operations for entity: Alden\xxxBundle\Entity\Study@0000000073eb1a8b00000000198469ff. Explicitly persist the new entity or configure cascading persist operations on the relationship. If you cannot find out which entity causes the problem implement 'Alden\xxxxBundle\Entity\Study#__toString()' to get a clue.

    So I added cascade to profile:

    /**
     * @var Study
     * @ORM\OneToMany(targetEntity="Study", mappedBy="profile", cascade={"persist"})
     */
    private $study;
    

    and than I got an error like in the begining: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'profile_id' cannot be null.

    edited My controller code:

        $request = $this->getRequest();
        $r = $this->getProfileRepository();
        $profile = $id ? $r->find($id) : new \Alden\BonBundle\Entity\Profile();
        /* @var $profile \Alden\BonBundle\Entity\Profile */
        $form = $this->createForm(new ProfileType(), $profile);
        if ($request->getMethod() == 'POST')
        {
            $form->bindRequest($request);
            if ($form->isValid())
            {
                $vacation = $profile->getVacation();
                foreach($vacation as $v) {
                    $r=5;
                }
                $em = $this->getEntityManager();
                $em->persist($profile);
                $em->flush();
                //return $this->redirect($this->generateUrl('profile_list'));
            }
        }
        return array(
            'profile' => $profile,
            'form' => $form->createView()
        );
    

    SOLUTION

    In Profile class, important parts are cascade, orphanRemoval in annotations and foreach loop in setStudies() (thanks to suggestion @Parhs)

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection
     * @ORM\OneToMany(targetEntity="Study", mappedBy="profile", cascade={"ALL"}, orphanRemoval=true)
     */
    private $studies;
    
    public function __construct()
    {
        $this->studies = new \Doctrine\Common\Collections\ArrayCollection();
    }
    
    /**
     * @return Doctrine\Common\Collections\Collection
     */
    public function getStudies()
    {
        return $this->studies;
    }
    
    public function setStudies($studies)
    {
        foreach ($studies as $v)
        {
            if (is_null($v->getProfile()))
            {
                $v->setProfile($this);
            }
        }
        $this->studies = $studies;
    }
    

    In Study class

    /**
     * @var Profile
     * @ORM\ManyToOne(targetEntity="Profile", inversedBy="studies")
     * @ORM\JoinColumn(name="profile_id", nullable=false, onDelete="CASCADE", referencedColumnName="profile_id")
     */
    private $profile;
    

    and usual getters and setters.