使用 Doctrine 删除与 Symfony 3 的 3 实体(一对多对一)关联

Delete a 3-entity (one-to-many-to-one) association with Symfony 3 using Doctrine

这是我的第一个问题!

我想关联两个实体:ProductCategory。一个商品可能有多个分类,一个分类可能对应多个商品。我决定将此关系实现为 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 CategoriesEntityType 字段,它映射到 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;
}