Symfony2 使用 Doctrine UNIQUE INDEX 和 ORDER BY 持久化表单集合?
Symfony2 persist form collections with Doctrine UNIQUE INDEX and ORDER BY?
我有一个 User 实体,它有一个与之关联的列表项集合。
每个列表项实体还引用另一个实体主题。我在列表项 table 上设置了一个 UNIQUE 约束,它只允许用户和主题外键的唯一组合。不允许每个用户使用重复引用主题实体的列表项。我也按 "completion_week".
排序结果
有时我会尝试保留一个表单集合,但它会因为违反完整性约束而失败。出于某种原因,Symfony 似乎认为正在对表单进行更新并且错误地尝试更新集合项 - 但似乎随机切换一些更新实体的外键 - 由于上述限制而导致错误.
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '6-1' for key 'list_item_user_topic'
用户实体:
<?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;
...
列表项实体:
<?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;
...
我正在使用 Symfony2 表单生成器来构建表单。这很好用。我在前端为 add/remove 按钮添加了 javascript。总的来说 - 我能够毫无问题地保存和保留表单集合。
用户表单类型:
<?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';
}
}
列表项表单类型:
<?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';
}
}
我发现,在处理表单时,handleRequest() 方法中发生了一些事情,该方法正在交换集合中不同列表项上的外键引用。在某些情况下 - 无需对前端的表单集合进行任何更改。像这样:
用户列表项的原始集合:
handleRequest() 后的用户列表项集合:
当 Doctrine 尝试写入第一条记录时,这会导致完整性约束冲突,因为它违反了列表项的唯一约束 table。我不明白的是 how/why handleRequest() 方法会在更新时交换外键。
此外 - 在许多情况下 - 表单对于用户来说将保持良好状态。我不喜欢在这里使用 "random" 这个词,但除了使用该实体一段时间并对其执行 CRUD 操作之外,我还没有找到一种方法来复制这个问题。很多时候表单仍然存在 - 其他时候外键引用被交换,由于 UNIQUE 约束,我无法提交表单来更新实体。
有没有人遇到过类似的问题或对为什么会发生这种情况有一些了解?这是 handleRequest() 方法中的错误吗?即使我没有对 List Item 集合进行任何更改,也会发生这种情况。如 - 如果我编辑一个用户并简单地提交表单而不做任何更改 - 这种行为仍然会发生。
有更好的方法吗?
解决方案是在 User 实体的 $listItems 属性 中添加一个 Doctrine "IndexBy" 注释。通过在此处指定一列,返回的结果将按其值进行索引。这必须是唯一值。在这种情况下,我使用了主键。
/**
* @ORM\OneToMany(
* targetEntity="App\MyBundle\Entity\ListItem",
* mappedBy="user",
* orphanRemoval=true,
* fetch="EAGER",
* indexBy="id",
* cascade={"all"}
* )
* @ORM\OrderBy({"completion_week"="ASC"})
*
*/
private $listItems;
这改变了在前端为每个集合项目编制索引的方式。
来自这里:
<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>
为此:
<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>
现在,在提交表单时,每个集合项都由其唯一 ID 引用 - 确保前端输入的数据在表单绑定后正确保存。
它表现得有些随机的原因是因为我按 "completion_week" 列对结果进行排序。记录有可能以不同的顺序返回,因为它们共享相同的 ORDER BY 值。如果您有三个具有相同值 "completion_week" 的记录并且您按 "completion_week" 排序 - 由 MySQL 决定结果的顺序。
当 Symfony 收到 POST 结果时 - 控制器必须再次调用数据库以获取用户实体并构建表单。如果结果以不同的顺序返回,从前端捕获的数组键将不匹配 - 并且在 Flush 上产生唯一约束错误。
另一种解决方案:将主键添加到 orderBy 条件
我有一个 User 实体,它有一个与之关联的列表项集合。
每个列表项实体还引用另一个实体主题。我在列表项 table 上设置了一个 UNIQUE 约束,它只允许用户和主题外键的唯一组合。不允许每个用户使用重复引用主题实体的列表项。我也按 "completion_week".
排序结果有时我会尝试保留一个表单集合,但它会因为违反完整性约束而失败。出于某种原因,Symfony 似乎认为正在对表单进行更新并且错误地尝试更新集合项 - 但似乎随机切换一些更新实体的外键 - 由于上述限制而导致错误.
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '6-1' for key 'list_item_user_topic'
用户实体:
<?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;
...
列表项实体:
<?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;
...
我正在使用 Symfony2 表单生成器来构建表单。这很好用。我在前端为 add/remove 按钮添加了 javascript。总的来说 - 我能够毫无问题地保存和保留表单集合。
用户表单类型:
<?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';
}
}
列表项表单类型:
<?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';
}
}
我发现,在处理表单时,handleRequest() 方法中发生了一些事情,该方法正在交换集合中不同列表项上的外键引用。在某些情况下 - 无需对前端的表单集合进行任何更改。像这样:
用户列表项的原始集合:
handleRequest() 后的用户列表项集合:
当 Doctrine 尝试写入第一条记录时,这会导致完整性约束冲突,因为它违反了列表项的唯一约束 table。我不明白的是 how/why handleRequest() 方法会在更新时交换外键。
此外 - 在许多情况下 - 表单对于用户来说将保持良好状态。我不喜欢在这里使用 "random" 这个词,但除了使用该实体一段时间并对其执行 CRUD 操作之外,我还没有找到一种方法来复制这个问题。很多时候表单仍然存在 - 其他时候外键引用被交换,由于 UNIQUE 约束,我无法提交表单来更新实体。
有没有人遇到过类似的问题或对为什么会发生这种情况有一些了解?这是 handleRequest() 方法中的错误吗?即使我没有对 List Item 集合进行任何更改,也会发生这种情况。如 - 如果我编辑一个用户并简单地提交表单而不做任何更改 - 这种行为仍然会发生。
有更好的方法吗?
解决方案是在 User 实体的 $listItems 属性 中添加一个 Doctrine "IndexBy" 注释。通过在此处指定一列,返回的结果将按其值进行索引。这必须是唯一值。在这种情况下,我使用了主键。
/**
* @ORM\OneToMany(
* targetEntity="App\MyBundle\Entity\ListItem",
* mappedBy="user",
* orphanRemoval=true,
* fetch="EAGER",
* indexBy="id",
* cascade={"all"}
* )
* @ORM\OrderBy({"completion_week"="ASC"})
*
*/
private $listItems;
这改变了在前端为每个集合项目编制索引的方式。
来自这里:
<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>
为此:
<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>
现在,在提交表单时,每个集合项都由其唯一 ID 引用 - 确保前端输入的数据在表单绑定后正确保存。
它表现得有些随机的原因是因为我按 "completion_week" 列对结果进行排序。记录有可能以不同的顺序返回,因为它们共享相同的 ORDER BY 值。如果您有三个具有相同值 "completion_week" 的记录并且您按 "completion_week" 排序 - 由 MySQL 决定结果的顺序。
当 Symfony 收到 POST 结果时 - 控制器必须再次调用数据库以获取用户实体并构建表单。如果结果以不同的顺序返回,从前端捕获的数组键将不匹配 - 并且在 Flush 上产生唯一约束错误。
另一种解决方案:将主键添加到 orderBy 条件