使用 Doctrine 删除与 Symfony 3 的 3 实体(一对多对一)关联
Delete a 3-entity (one-to-many-to-one) association with Symfony 3 using Doctrine
这是我的第一个问题!
我想关联两个实体:Product
和 Category
。一个商品可能有多个分类,一个分类可能对应多个商品。我决定将此关系实现为 3-class 关联,具有中间 ProductCategory
实体,如下图所示。这使我可以灵活地在将来向关联添加属性。
Representation of my tree-class association
我想将现有类别分配给现有产品。 我想在实体内部建立关系。我能够在 Product
实体中执行此操作,使用 setter 方法接收 Category
实体数组,并为每个传递的类别创建一个新的 ProductCategory
实体。程序如下:
//Product.php
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
请注意,我在添加新集合之前清除了 ProductCategory
集合;这样,只有在表格中选择的那些类别才会保存到数据库中。
我的问题是 Doctrine 在插入新记录之前不会从数据库中删除记录。当没有为产品分配类别时这很好,但我在尝试更新关联时得到 Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'PRIMARY'
。我检查了 Doctrine 部分中的 Symfony 调试面板,在插入之前没有执行任何 DELETE 语句。
是否可以从一个实体中删除相关实体?如果没有,那么为什么可以添加新的?提前致谢。
我的实体如下:
Product.php:
namespace TestBundle\Entity;
/**
* @ORM\Table(name="product")
* @ORM\Entity(repositoryClass="TestBundle\Repository\ProductRepository")
*/
class Product {
/**
* @var int
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
* @ORM\OneToMany(targetEntity="ProductCategory", mappedBy="product", cascade={"persist"})
*/
private $productCategory;
/**
* Constructor
*/
public function __construct() {
$this->productCategory = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* @param \TestBundle\Entity\ProductCategory $productCategory
* @return Product
*/
public function addProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory[] = $productCategory;
return $this;
}
/**
* @param \TestBundle\Entity\ProductCategory $productCategory
*/
public function removeProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory->removeElement($productCategory);
}
/**
* @return \Doctrine\Common\Collections\Collection
*/
public function getProductCategory() {
return $this->productCategory;
}
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
/**
* @return \Doctrine\Common\Collections\ArrayCollection
*/
public function getCategories() {
$categories = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->getProductCategory() as $pc) {
$categories[] = $pc->getCategory();
}
return $categories;
}
}
Category.php:
namespace TestBundle\Entity;
/**
* @ORM\Table(name="category")
* @ORM\Entity(repositoryClass="TestBundle\Repository\CategoryRepository")
*/
class Category {
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
* @ORM\OneToMany(targetEntity="ProductCategory", mappedBy="category", cascade={"persist"})
*/
private $productCategory;
}
产品Category.php
namespace TestBundle\Entity;
/**
* @ORM\Table(name="product_category")
* @ORM\Entity(repositoryClass="TestBundle\Repository\ProductCategoryRepository")
*/
class ProductCategory {
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity="Product", inversedBy="productCategory")
* @ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity="Category", inversedBy="productCategory")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}
我的Product
表单生成如下:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('categories', EntityType::class, array(
'class' => 'TestBundle:Category',
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
));
}
请注意,我使用 categories
字段名称,该名称将填充取自 Category
实体的类别。形式 returns 一个 Category
对象的数组,我用它在 Product.php
.
的 setCategories()
方法中生成 ProductCategory
实体
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
编辑 1:
我在 Product
中没有 categories
字段,我只有 getCategories()
和 setCategories()
方法。如我的表单类型代码所示,我添加了 class Categories
的 EntityType
字段,它映射到 categories
属性(实际上并不存在).通过这种方式,我可以将现有类别显示为复选框,并且产品的类别已被正确选中。
编辑 2:可能的解决方案
我最终听从了 Sam Jenses 的建议。我创建了一个服务如下:
文件:src/TestBundle/Service/CategoryCleaner.php
namespace TestBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
use TestBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Request;
class CategoryCleaner {
/**
*
* @var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function cleanCategories(Product $product, Request $request) {
if ($this->em == null) {
throw new Exception('Entity manager parameter must not be null');
}
if ($request == null) {
throw new Exception('Request parameter must not be null');
}
if ($request->getMethod() == 'POST') {
$categories = $this->em->getRepository('TestBundle:ProductCategory')->findByProduct($product);
foreach ($categories as $category) {
$this->em->remove($category);
}
$this->em->flush();
}
}
}
在接收当前Product和Request作为参数的cleanCategories
方法中,删除了ProductCategory
对应Product
的所有条目,只有在POST请求。
服务注册如下:
文件app/config/services.yml
services:
app.category_cleaner:
class: TestBundle\Service\CategoryCleaner
arguments: ['@doctrine.orm.entity_manager']
必须在handleRequest($request)
之前从控制器调用服务,即在添加新类别之前。如果不是,我们会得到一个重复条目异常。
从文件编辑方法 TestBundle/Controller/ProductController.php
public function editAction(Request $request, Product $product) {
$deleteForm = $this->createDeleteForm($product);
$editForm = $this->createForm('TestBundle\Form\ProductType', $product);
$this->container->get('app.category_cleaner')->cleanCategories($product, $request);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('product_edit', array('id' => $product->getId()));
}
return $this->render('product/edit.html.twig', array(
'product' => $product,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
请验证我的方法。
创建一个中间服务,您也可以在其中使用 doctrine 删除现有实体
我想你的实体内部有一些方法,比如:
addCategory
removeCategory
getCategory
还有
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
所以在你的函数中你可以做:
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->categories as $category) {
$this->removeCategory($category);
}
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
这是我的第一个问题!
我想关联两个实体:Product
和 Category
。一个商品可能有多个分类,一个分类可能对应多个商品。我决定将此关系实现为 3-class 关联,具有中间 ProductCategory
实体,如下图所示。这使我可以灵活地在将来向关联添加属性。
Representation of my tree-class association
我想将现有类别分配给现有产品。 我想在实体内部建立关系。我能够在 Product
实体中执行此操作,使用 setter 方法接收 Category
实体数组,并为每个传递的类别创建一个新的 ProductCategory
实体。程序如下:
//Product.php
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
请注意,我在添加新集合之前清除了 ProductCategory
集合;这样,只有在表格中选择的那些类别才会保存到数据库中。
我的问题是 Doctrine 在插入新记录之前不会从数据库中删除记录。当没有为产品分配类别时这很好,但我在尝试更新关联时得到 Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'PRIMARY'
。我检查了 Doctrine 部分中的 Symfony 调试面板,在插入之前没有执行任何 DELETE 语句。
是否可以从一个实体中删除相关实体?如果没有,那么为什么可以添加新的?提前致谢。
我的实体如下:
Product.php:
namespace TestBundle\Entity;
/**
* @ORM\Table(name="product")
* @ORM\Entity(repositoryClass="TestBundle\Repository\ProductRepository")
*/
class Product {
/**
* @var int
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
* @ORM\OneToMany(targetEntity="ProductCategory", mappedBy="product", cascade={"persist"})
*/
private $productCategory;
/**
* Constructor
*/
public function __construct() {
$this->productCategory = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* @param \TestBundle\Entity\ProductCategory $productCategory
* @return Product
*/
public function addProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory[] = $productCategory;
return $this;
}
/**
* @param \TestBundle\Entity\ProductCategory $productCategory
*/
public function removeProductCategory(\TestBundle\Entity\ProductCategory $productCategory) {
$this->productCategory->removeElement($productCategory);
}
/**
* @return \Doctrine\Common\Collections\Collection
*/
public function getProductCategory() {
return $this->productCategory;
}
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
/**
* @return \Doctrine\Common\Collections\ArrayCollection
*/
public function getCategories() {
$categories = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->getProductCategory() as $pc) {
$categories[] = $pc->getCategory();
}
return $categories;
}
}
Category.php:
namespace TestBundle\Entity;
/**
* @ORM\Table(name="category")
* @ORM\Entity(repositoryClass="TestBundle\Repository\CategoryRepository")
*/
class Category {
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
* @ORM\OneToMany(targetEntity="ProductCategory", mappedBy="category", cascade={"persist"})
*/
private $productCategory;
}
产品Category.php
namespace TestBundle\Entity;
/**
* @ORM\Table(name="product_category")
* @ORM\Entity(repositoryClass="TestBundle\Repository\ProductCategoryRepository")
*/
class ProductCategory {
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity="Product", inversedBy="productCategory")
* @ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity="Category", inversedBy="productCategory")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}
我的Product
表单生成如下:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('categories', EntityType::class, array(
'class' => 'TestBundle:Category',
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
));
}
请注意,我使用 categories
字段名称,该名称将填充取自 Category
实体的类别。形式 returns 一个 Category
对象的数组,我用它在 Product.php
.
setCategories()
方法中生成 ProductCategory
实体
/**
* @param \Doctrine\Common\Collections\ArrayCollection $categories
* @return \TestBundle\Entity\Product
*/
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}
编辑 1:
我在 Product
中没有 categories
字段,我只有 getCategories()
和 setCategories()
方法。如我的表单类型代码所示,我添加了 class Categories
的 EntityType
字段,它映射到 categories
属性(实际上并不存在).通过这种方式,我可以将现有类别显示为复选框,并且产品的类别已被正确选中。
编辑 2:可能的解决方案
我最终听从了 Sam Jenses 的建议。我创建了一个服务如下:
文件:src/TestBundle/Service/CategoryCleaner.php
namespace TestBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
use TestBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Request;
class CategoryCleaner {
/**
*
* @var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function cleanCategories(Product $product, Request $request) {
if ($this->em == null) {
throw new Exception('Entity manager parameter must not be null');
}
if ($request == null) {
throw new Exception('Request parameter must not be null');
}
if ($request->getMethod() == 'POST') {
$categories = $this->em->getRepository('TestBundle:ProductCategory')->findByProduct($product);
foreach ($categories as $category) {
$this->em->remove($category);
}
$this->em->flush();
}
}
}
在接收当前Product和Request作为参数的cleanCategories
方法中,删除了ProductCategory
对应Product
的所有条目,只有在POST请求。
服务注册如下:
文件app/config/services.yml
services:
app.category_cleaner:
class: TestBundle\Service\CategoryCleaner
arguments: ['@doctrine.orm.entity_manager']
必须在handleRequest($request)
之前从控制器调用服务,即在添加新类别之前。如果不是,我们会得到一个重复条目异常。
从文件编辑方法 TestBundle/Controller/ProductController.php
public function editAction(Request $request, Product $product) {
$deleteForm = $this->createDeleteForm($product);
$editForm = $this->createForm('TestBundle\Form\ProductType', $product);
$this->container->get('app.category_cleaner')->cleanCategories($product, $request);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('product_edit', array('id' => $product->getId()));
}
return $this->render('product/edit.html.twig', array(
'product' => $product,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
请验证我的方法。
创建一个中间服务,您也可以在其中使用 doctrine 删除现有实体
我想你的实体内部有一些方法,比如:
addCategory
removeCategory
getCategory
还有
public function __construct()
{
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
所以在你的函数中你可以做:
public function setCategories($categories) {
$productCategoryReplacement = new \Doctrine\Common\Collections\ArrayCollection();
foreach ($this->categories as $category) {
$this->removeCategory($category);
}
foreach ($categories as $category) {
$newProductCategory = new ProductCategory();
$newProductCategory->setProduct($this);
$newProductCategory->setCategory($category);
$productCategoryReplacement[] = $newProductCategory;
}
$this->productCategory = $productCategoryReplacement;
return $this;
}