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
相关产品推荐
相关产品推荐

