OneToMany relationship not persisting along with n

2019-04-12 11:50发布

I'm facing some issue when I try to persist a collection of entities using a symfony form. I followed the official documentation but I can't make it work becouse of this error:

Entity of type ProductItem has identity through a
foreign entity Product, however this entity has no identity itself. You have to call    
EntityManager#persist() on the related  entity and make sure that an identifier was 
generated before trying to persist ProductItem. In case of Post Insert ID 
Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you 
have to call EntityManager#flush() between both persist operations.

I have to entities linked with a OneToMany relation:

Product

/**
 * @ORM\Column(name="id", type="integer", nullable=false)
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="IDENTITY")
 */
protected $id;

/**
 * @ORM\OneToMany(targetEntity="ProductItem", mappedBy="product",cascade={"persist"})
 */
protected $items;

And ProductItem

/**
 * @ORM\Id()
 * @ORM\ManyToOne(targetEntity="Product", inversedBy="items")
 */
protected $product;

/**
 * @ORM\Id()
 * @ORM\ManyToOne(targetEntity="Item")
 */
protected $item;

This is how it is added to the form:

->add('items','collection',array(
            'label' => false,
            'type' => new ProductItemType(),
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false))

And this is the controller action:

public function newAction()
{
    $product= new Product();

    $form = $this->createForm(new ProductType(), $product);
    if($request->isMethod("POST"))
    {
        $form->handleRequest($request);
        if($form->isValid())
        {
            $em = $this->getDoctrine()->getManager();
            $em->persist($product);
            $em->flush();
        }
    }
}

I'm doing something wrong for sure in the controller because, as the error message says, I have to persist $product before adding $productItems, but how can I do that?

I only get this error when trying to persist a new entity, if the entity has been persisted before, I can add as may items as I want successfully

5条回答
Animai°情兽
2楼-- · 2019-04-12 12:31

Also faced this issue during the work with form to which CollectionType field was attached. The other one approach which could solve this problem and also mentioned in doctrine official documentation is following:

public function newAction()
{
    $product= new Product();

    $form = $this->createForm(new ProductType(), $product);
    if($request->isMethod("POST"))
    {
        $form->handleRequest($request);
        if($form->isValid())
        {
            foreach ($product->getItems() as $item)
            {
                $item->setProduct($product);
            }

            $em = $this->getDoctrine()->getManager();
            $em->persist($product);
            $em->flush();
        }
    }
}

In simple words, you should provide product link to linked items manually - this is described in "Establishing associations" section of following article: http://docs.doctrine-project.org/en/latest/reference/working-with-associations.html#working-with-associations

查看更多
唯我独甜
3楼-- · 2019-04-12 12:32

You didn't follow the docs completely. Here is something you can do to test a single item, but if you want to dynamically add and delete items (it looks like you do), you will also need to implement all the javascript that is included in the docs that you linked to.

$product= new Product();
$productItem = new ProductItem();

// $items must be an arraycollection
$product->getItems()->add($productItem);

$form = $this->createForm(new ProductType(), $product);
if($request->isMethod("POST"))
{
    $form->handleRequest($request);
    if($form->isValid())
    {
        $em = $this->getDoctrine()->getManager();
        $em->persist($productItem);
        $em->persist($product);
        $em->flush();
    }
}

So this should work for a single static item, but like I said, the dynamic stuff is a bit more work.

查看更多
Evening l夕情丶
4楼-- · 2019-04-12 12:32

IMO, your problem is not related to your controller but to your Entities. It seems your would like to make a ManyToMany between your Product and Item and not creating a ProductItem class which should behave as an intermediate object for representing your relation. Additionally, this intermediate object have no id generation strategy. This is why Doctrine explains you, you must first persist/flush all your new items and then persist/flush your product in order to be able to get the ids for the intermediate object.

查看更多
欢心
5楼-- · 2019-04-12 12:37

The annotation is wrong... the cascade persist is on the wrong side of the relation

/**
 * @ORM\OneToMany(targetEntity="ProductItem", mappedBy="product")
 */
protected $items;


/**
 * @ORM\Id()
 * @ORM\ManyToOne(targetEntity="Product", inversedBy="items", cascade={"persist"})
 */
protected $product;

Another way to achieve this (e.g. annotation not possible) is to set the form by_reference

查看更多
Root(大扎)
6楼-- · 2019-04-12 12:54

I had exact same problem last week, here is a solution I found after some reading and testing.

The problem is your Product entity has cascade persist (which is usually good) and it first try to persist ProductItem but ProductItem entities cannot be persisted because they require Product to be persisted first and its ID (Composite key (product, item).

There are 2 options to solve this:

1st I didn't use it but you could simply drop a composite key and use standard id with foreign key to the Product

2nd - better This might look like hack, but trust me this is the best what you can do now. It doesn't require any changes to the DB structure and works with form collections without any problems.

Code fragment from my code, article sections have composite key of (article_id, random_hash). Temporary set one to many reference to an empty array, persist it, add you original data and persist (and flush) again.

    if ($form->isValid())
    {
        $manager = $this->getDoctrine()->getManager();

        $articleSections = $article->getArticleSections();
        $article->setArticleSections(array());  // this won't trigger cascade persist
        $manager->persist($article);
        $manager->flush();

        $article->setArticleSections($articleSections);
        $manager->persist($article);
        $manager->flush();
查看更多
登录 后发表回答