duanqing2209 2015-02-21 23:17
浏览 59
已采纳

Symfony2使用Doctrine UNIQUE INDEX和ORDER BY持久保存表单集合?

I have a User entity that has a collection of List Items associated with it.

Each List Item entity also references another entity, Topic. I have setup a UNIQUE constraint on the List Item table that will only allow unique combinations of the User and Topic foreign keys. No List Items with a duplicate reference to the Topic entity are allowed for each user. I am also ordering the results by "completion_week".

There are times when I will be attempting to persist a form collection and it will fail with an integrity constraint violation. For some reason Symfony seems to think updates are being made to the form and is incorrectly attempting to update collection items - but is switching the a foreign key on some of the updated entities seemingly randomly - which is causing the error because of the above mentioned constraints.

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '6-1' for key 'list_item_user_topic'

The User Entity:

<?php
/**
 * @ORM\Entity(repositoryClass="App\MyBundle\Repository\UserRepository")
 * @ORM\Table(name="users")
 * @Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=true)
 */
class User implements UserInterface, EquatableInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=30, nullable=false)
     */
    private $first_name;

    /**
     * @ORM\Column(type="string", length=30, nullable=false)
     */
    private $last_name;

    /**
     * @ORM\Column(type="string", unique=true, length=100, nullable=true)
     */
    private $email;

    /**
     * @ORM\OneToMany(
     *     targetEntity="App\MyBundle\Entity\ListItem",
     *     mappedBy="user",
     *     orphanRemoval=true,
     *     fetch="EAGER",
     *     cascade={"all"}
     * )
     * @ORM\OrderBy({"completion_week"="ASC"})
     * 
     */
    private $listItems;

    ...

The List Item Entity:

<?php
/**
 * @ORM\Entity(repositoryClass="App\MyBundle\Repository\ListItemRepository")
 * @ORM\Table(
 *     name="list_items",
 *     uniqueConstraints={@ORM\UniqueConstraint(name="list_item_user_topic", columns={"user_id","topic_id"})}
 * )
 *
 * @Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=true)
 */
class ListItem
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\MyBundle\Entity\User", inversedBy="listItems", fetch="EAGER")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     */
    private $user;

    /**
     * @ORM\Column(type="integer", length=11, nullable=true)
     */
    private $completion_week;

    /**
     * @ORM\ManyToOne(targetEntity="App\MyBundle\Entity\Topic", inversedBy="listItems", fetch="EAGER")
     * @ORM\JoinColumn(name="topic_id", referencedColumnName="id")
     */
    private $topic;

    ...

I am using Symfony2 form builder to build the form. This is working great. I have added javascript for add/remove buttons on the front end. In general - I am able to save and persist the form collection without any problems.

User Form Type:

<?php
/**
 * Class UserType
 * @package App\MyBundle\Form\Type
 */
class UserType extends AbstractType
{
    /**
     * @param OptionsResolverInterface $resolver]
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'            => 'App\MyBundle\Entity\User',
            'method'                => 'POST',
            'cascade_validation'    => true
        ));
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Set User data
        $user = $builder->getData();

        // Generate form
        $builder
            ->add('listItems', 'collection', array(
                'options'  => array(
                    'required' => false,
                    'attr' => array('class'=>'col-sm-12')
                ),
                'type'              => new ListItemType(),
                'label'             => false,
                'allow_add'         => true,
                'allow_delete'      => true,
                'delete_empty'      => true,
                'prototype'         => true,
                'by_reference'      => false
            ))
            ->add('first_name')
            ->add('last_name')
            ->add('email');
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'user';
    }
}

List Item Form Type:

<?php
/**
 * Class ListItemType
 * @package App\MyBundle\Form\Type
 */
class ListItemType extends AbstractType
{
    /**
     * @param OptionsResolverInterface $resolver]
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'                => 'App\MyBundle\Entity\ListItem',
            'method'                    => 'POST',
        ));
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Generate form
        $builder
            ->add('topic', 'entity', array(
                'attr'          => array('class' => 'form-control chosen-select-10'),
                'class'         => 'AppMyBundle:Topic',
                'empty_value'   => 'Choose a Topic',
                'label'         => false,
                'property'      => 'name',
                'expanded'      => false,
                'multiple'      => false
            ))
            ->add('completion_week', 'integer', array(
                'attr'          => array('class' => 'form-control'),
                'label'         => false,
            ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'list_item';
    }
}

What I discovered is that when the form is being processed - something is happening within the handleRequest() method that is swapping out foreign key references on different list items in the collection. In some cases - without making any changes to the form collection on the front end. Like so:

Original collection of List Items for a User:
Original collection of List Items for a User

User's List Item Collection after handleRequest():
User's List Item Collection after handleRequest()

This then causes the integrity constraint violation when Doctrine attempts to write the first record because it is violating the unique constraint on the List Items table. What I do not understand is how/why the handleRequest() method would be swapping foreign keys on update.

Also - in many cases - the form will persist fine for a user. I hate to use the word "random" here but I have not been able to identify a way to duplicate the issue other than just working with the entity for a while and performing CRUD operations on it. Many times the form persists fine - other times the foreign key references get swapped and I am unable to submit the form to update the entity because of the UNIQUE constraint.

Has anyone experienced similar issues or have some insight on why this might be occurring? Is this a bug in the handleRequest() method? This will occur even if I have not made any changes to the List Item collection. As in - if I edit a user and simply submit the form without making any changes - this behavior will still occur.

Is there a better way to do this?

  • 写回答

1条回答 默认 最新

  • dow46218 2015-02-24 06:13
    关注

    The solution was to add a Doctrine "IndexBy" annotation to the $listItems property on the User entity. By specifying a column here, the results returned will be indexed by its value. This must be a unique value. In this case I used the primary key.

    /**
     * @ORM\OneToMany(
     *     targetEntity="App\MyBundle\Entity\ListItem",
     *     mappedBy="user",
     *     orphanRemoval=true,
     *     fetch="EAGER",
     *     indexBy="id",
     *     cascade={"all"}
     * )
     * @ORM\OrderBy({"completion_week"="ASC"})
     * 
     */
    private $listItems;
    

    This then changed the way each of the collection items were indexed on the frontend.

    From this:

    <div class="row" data-content="user[listItems][0]">...</div>
    <div class="row" data-content="user[listItems][1]">...</div>
    <div class="row" data-content="user[listItems][2]">...</div>
    <div class="row" data-content="user[listItems][3]">...</div>
    <div class="row" data-content="user[listItems][4]">...</div>
    

    To this:

    <div class="row" data-content="user[listItems][1950]">...</div>
    <div class="row" data-content="user[listItems][1951]">...</div>
    <div class="row" data-content="user[listItems][1955]">...</div>
    <div class="row" data-content="user[listItems][1953]">...</div>
    <div class="row" data-content="user[listItems][1948]">...</div>
    

    Now, when submitting the form, each collection item is referenced by it unique ID - ensuring that the data input on the frontend is persisted correctly after form binding.

    The reason it was behaving somewhat randomly was because I was ordering the results by the "completion_week" column. There was a chance for the records to be returned in a different order where they share the same ORDER BY value. If you have three records with the same value for "completion_week" and you ORDER BY "completion_week" - it's up to MySQL to determine the order of the results.

    When Symfony received the POST results - the controller had to make another call to the database to get the User entity and build the form. If the results were returned in a different order, the array keys captured from the frontend would not match up - and the unique constraint error was produced onFlush.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 安卓adb backup备份应用数据失败
  • ¥15 eclipse运行项目时遇到的问题
  • ¥15 关于#c##的问题:最近需要用CAT工具Trados进行一些开发
  • ¥15 南大pa1 小游戏没有界面,并且报了如下错误,尝试过换显卡驱动,但是好像不行
  • ¥15 没有证书,nginx怎么反向代理到只能接受https的公网网站
  • ¥50 成都蓉城足球俱乐部小程序抢票
  • ¥15 yolov7训练自己的数据集
  • ¥15 esp8266与51单片机连接问题(标签-单片机|关键词-串口)(相关搜索:51单片机|单片机|测试代码)
  • ¥15 电力市场出清matlab yalmip kkt 双层优化问题
  • ¥30 ros小车路径规划实现不了,如何解决?(操作系统-ubuntu)