Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composite key and form

I have the following associations in my database (simplified version):

db schema

This is a Many-To-Many association but with an attribute on the joining table, so I have to use One-To-Many/Many-To-One associations.

I have a form where I can add as many relations as I want to one order item and create it at the same time (mainly inspired by the How to Embed a Collection of Forms tutorial from the documentation.

When I post the form, I get the following error:

Entity of type TEST\MyBundle\Entity\Relation has identity through a foreign entity TEST\MyBundle\Entity\Order, 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 'TEST\MyBundle\Entity\Relation'. 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 understand this error because Doctrine tries to persist the Relation object(s) related to the order since I have the cascade={"persist"} option on the OneToMany relation. But how can I avoid this behavior?

I have tried to remove cascade={"persist"} and manually persist the entity, but I get the same error (because I need to flush() order to get the ID and when I do so, I have the same error message).
I also tried to detach() the Relation objects before the flush() but with no luck.

like image 397
cheesemacfly Avatar asked Apr 09 '13 18:04

cheesemacfly


3 Answers

This problem seems unique if 1) you are using a join table with composite keys, 2) forms component, and 3) the join table is an entity that is being built by the form component's 'collection' field. I saw a lot of people having problems but not a lot of solutions, so I thought I'd share mine.

I wanted to keep my composite primary key, as I wanted to ensure that only one instance of the two foreign keys would persist in the database. Using this entity setup as an example

/** @Entity */
class Order
{
    /** @OneToMany(targetEntity="OrderItem", mappedBy="order") */
    private $items;

    public function __construct(Customer $customer)
    {
        $this->items = new Doctrine\Common\Collections\ArrayCollection();
    }
}

/** @Entity */
class Product
{
    /** @OneToMany(targetEntity="OrderItem", mappedBy="product") */
    private $orders;
    .....

    public function __construct(Customer $customer)
    {
        $this->orders = new Doctrine\Common\Collections\ArrayCollection();
    }
}

/** @Entity */
class OrderItem
{
    /** @Id @ManyToOne(targetEntity="Order") */
    private $order;

    /** @Id @ManyToOne(targetEntity="Product") */
    private $product;

    /** @Column(type="integer") */
    private $amount = 1;
}

The problem I was facing, if I were building an Order object in a form, that had a collection field of OrderItems, I wouldn't be able to save OrderItem entity without having saved the Order Entity first (as doctrine/SQL needs the order id for the composite key), but the Doctrine EntityManager wasn't allowing me to save the Order object that has OrderItem attributes (because it insists on saving them en mass together). You can't turn off cascade as it will complain that you haven't saved the associated entities first, and you cant save the associated entities before saving Order. What a conundrum. My solution was to remove the associated entities, save Order and then reintroduce the associated entities to the Order object and save it again. So first I created a mass assignment function of the ArrayCollection attribute $items

class Order
{
    .....
    public function setItemsArray(Doctrine\Common\Collections\ArrayCollection $itemsArray = null){
    if(null){
        $this->items->clear();
    }else{
        $this->items = $itemsArray;
    }
    ....
}

And then in my Controller where I process the form for Order.

//get entity manager
$em = $this->getDoctrine()->getManager();
//get order information (with items)
$order = $form->getData();
//pull out items array from order
$items = $order->getItems();
//clear the items from the order
$order->setItemsArray(null);
//persist and flush the Order object
$em->persist($order);
$em->flush();

//reintroduce the order items to the order object
$order->setItemsArray($items);
//persist and flush the Order object again ):
$em->persist($order);
$em->flush();

It sucks that you have to persist and flush twice (see more here Persist object with two foreign identities in doctrine). But that is doctrine for you, with all of it's power, it sure can put you in a bind. But thankfully you will only have to do this when creating a new object, not editing, because the object is already in the database.

like image 133
JTG Avatar answered Oct 16 '22 19:10

JTG


You need to persist and flush the original before you can persist and flush the relationship records. You are 100% correct in the reason for the error.

I assume from the diagram that you are trying to add and order and the relation to the contact at the same time? If so you need to persist and flush the order before you can persist and flush the relationship. Or you can add a primary key to the Relation table.

like image 31
Fred Jiles Avatar answered Oct 16 '22 18:10

Fred Jiles


I ended up creating a separated primary key on my Relation table (instead of having the composite one).
It looks like it is a dirty fix, and I am sure there is a better way to handle this situation but it works for now.

Here is my Relations entity:

/**
 * Relation
 *
 * @ORM\Entity
 */
class Relation
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Contact", inversedBy="relation")
     */
    protected $contact;

    /**
     * @ORM\ManyToOne(targetEntity="Order", inversedBy="relation")
     */
    protected $order;

    /**
     * @var integer
     *
     * @ORM\Column(name="invoice", type="integer", nullable=true)
     */
    private $invoice;

    //Rest of the entity...

I then added the cascade={"persist"} option on the OneToMany relation with Order:

/**
 * Orders
 *
 * @ORM\Entity
 */
class Order
{   
    /**
     * @ORM\OneToMany(targetEntity="Relation", mappedBy="order", cascade={"persist"})
     */
    protected $relation;

    //Rest of the entity...

Et voilà!

like image 1
cheesemacfly Avatar answered Oct 16 '22 17:10

cheesemacfly