Symfony2 form and Doctrine2 - update foreign key i

2019-02-11 00:55发布

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.

3条回答
Deceive 欺骗
2楼-- · 2019-02-11 01:34

Your solution did not work for me. In fact I had already set up the relationship and defined the adder, remover, getter and after I saw your solution i added the setter.

However my setter was not called when the form was submitted.

The solution for me was another one.

In the adder method I added a call to set the owner.

public function addStudie($studie)
{
     $studie->setProfile($this);
     $this->studies->add($studie);
}

And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.

   $builder->add('study', 'collection', array(
                'type' => new StudyType(),
                'allow_add' => true,
                `by_reference` => false,
                'allow_delete' => true
                    )
            )

Here is the documentation:

Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

查看更多
Animai°情兽
3楼-- · 2019-02-11 01:39

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.

查看更多
【Aperson】
4楼-- · 2019-02-11 01:42

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

查看更多
登录 后发表回答