You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony ManyToMany无需调用persist?调用后出现重复数据疑问

Symfony多对多关联表单持久化问题

实体与表单定义

Domain实体

class Domain
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 191, unique: true)]
    #[Assert\NotBlank(message: 'domain.name.not_blank')]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: Bill::class, inversedBy: 'domains', cascade: ['persist', 'remove'])]
    private Collection $bill;

    public function __construct()
    {
        $this->bill = new ArrayCollection();
    }

    public function __toString()
    {
        return $this->name;
    }

    /**
     * @return Collection<int, Bill>
     */
    public function getBill(): Collection
    {
        return $this->bill;
    }

    public function addBill(Bill $bill): self
    {
        if (!$this->bill->contains($bill)) {
            $this->bill->add($bill);
        }

        return $this;
    }

    public function removeBill(Bill $bill): self
    {
        $this->bill->removeElement($bill);

        return $this;
    }
}

Bill实体

class Bill
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $number = null;

    #[ORM\ManyToMany(targetEntity: Domain::class, mappedBy: 'bill')]
    private Collection $domains;

    public function __construct()
    {
        $this->domains = new ArrayCollection();
    }

    public function __toString()
    {
        return $this->number;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getNumber(): ?string
    {
        return $this->number;
    }

    public function setNumber(string $number): self
    {
        $this->number = $number;

        return $this;
    }

    /**
     * @return Collection<int, Domain>
     */
    public function getDomains(): Collection
    {
        return $this->domains;
    }

    public function addDomain(Domain $domain): self
    {
        if (!$this->domains->contains($domain)) {
            $this->domains->add($domain);
            $domain->addBill($this);
        }

        return $this;
    }

    public function removeDomain(Domain $domain): self
    {
        if ($this->domains->removeElement($domain)) {
            $domain->removeBill($this);
        }

        return $this;
    }
}

BillType表单

class BillingType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('number', TextType::class, [
                'attr' => ['placeholder' => 'Numero fattura',]
            ])
            ->add('domains', EntityType::class, [
                'class' => Domain::class,
                'expanded' => true,
                'multiple' => true,
                'choice_label' => 'name'
            ])
            ->add('submit', SubmitType::class, [
                'attr' => [
                    'class' => 'btn-success',
                ]
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Bill::class,
        ]);
    }
}

控制器代码及问题

尝试为多个Domain设置关联Bill的控制器代码:

public function domainBilling(Request $request, ManagerRegistry $doctrine, PaginatorInterface $paginator)
{
    $filters = $this->getFilters($request)['filters'];
    $form = $this->getFilters($request)['form'];

    $dbquery = $doctrine
        ->getRepository(Domain::class)
        ->findAllPaginated($filters)
        ->getQuery();

    $domains = $paginator->paginate(
        $dbquery, /* query NOT result */
        $request->query->getInt('page', 1), /*page number*/
        Domain::perPage, /*limit per page*/
        [
            'defaultSortFieldName' => 'd.name',
            'defaultSortDirection' => 'asc',
        ]
    );

    $billing_form = $this->createForm(BillingType::class);
    $billing_form->handleRequest($request);

    if ($billing_form->isSubmitted() && $billing_form->isValid()) {
        $em = $doctrine->getManager();

        $domains = $billing_form['domains']->getData();
        $bill_number = $billing_form['number']->getData();

        $bill = new Bill();
        $bill->setNumber($bill_number);
        
        $em->flush();
        $this->addFlash(
            'success',
            'Le modifiche sono state effettuate!'
        );
        return $this->redirectToRoute('domain_dashboard');
    }

    return $this->render('Admin/domain_dashboard.html.twig', [
        'billing_form' => $billing_form->createView(),
        'form' => $form->createView(),
        'domains' => $domains
    ]);
}

问题:目前无需调用$em->persist()对Bill或Domain进行持久化,也无需循环保存每个Domain,代码即可正常工作。但如果调用persist,数据库中会出现Bill的重复条目。请问操作是否有误?


问题解析与修正

1. 核心问题:控制器重复创建Bill实例

表单的data_class设置为Bill::class,调用handleRequest($request)后,表单会自动创建并填充完整的Bill对象(包含提交的number和选中的domains关联)。但你手动新建了另一个Bill实例,这部分代码完全冗余,且该实例未关联任何Domain,属于无效操作。

2. 无需手动persist()的原因

Domain实体的bill字段配置了cascade: ['persist', 'remove'],而表单中选中的Domain都是从数据库加载的托管实体(Doctrine已跟踪其状态)。当表单将Domain关联到自动生成的Bill时,Domain的addBill()方法会将Bill加入自身关联集合,Doctrine会通过cascade: persist自动识别这个新Bill,并在flush()时将其持久化,因此无需手动调用persist()。

3. persist()导致重复条目的原因

如果手动调用$em->persist($bill)(这里的$bill是你手动新建的实例),会生成两个Bill条目:

  • 一个是表单自动生成的Bill,通过Domain的cascade: persist被持久化;
  • 另一个是你手动创建的Bill,通过persist()被显式持久化。
    两者number相同,因此出现重复。

修正后的控制器代码

public function domainBilling(Request $request, ManagerRegistry $doctrine, PaginatorInterface $paginator)
{
    $filters = $this->getFilters($request)['filters'];
    $form = $this->getFilters($request)['form'];

    $dbquery = $doctrine
        ->getRepository(Domain::class)
        ->findAllPaginated($filters)
        ->getQuery();

    $domains = $paginator->paginate(
        $dbquery,
        $request->query->getInt('page', 1),
        Domain::perPage,
        [
            'defaultSortFieldName' => 'd.name',
            'defaultSortDirection' => 'asc',
        ]
    );

    $billing_form = $this->createForm(BillingType::class);
    $billing_form->handleRequest($request);

    if ($billing_form->isSubmitted() && $billing_form->isValid()) {
        $em = $doctrine->getManager();
        // 直接获取表单处理后的Bill对象,无需手动创建
        $bill = $billing_form->getData();
        
        // 无需调用persist,cascade配置会自动处理持久化
        $em->flush();
        
        $this->addFlash(
            'success',
            '修改已生效!'
        );
        return $this->redirectToRoute('domain_dashboard');
    }

    return $this->render('Admin/domain_dashboard.html.twig', [
        'billing_form' => $billing_form->createView(),
        'form' => $form->createView(),
        'domains' => $domains
    ]);
}

内容的提问来源于stack exchange,提问作者Daniel Gavriliuc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 04:40:27