Symfony2 Many-to-Many relationship sharing one JoinTable - symfony

I want to make a Many-to-Many relationship which shares the same join table. I tried the following:
<?php
/** #Entity **/
class User
{
// ...
/**
* #ManyToMany(targetEntity="Group", inversedBy="users")
* #JoinTable(name="users_groups")
**/
private $groups;
// ...
}
/** #Entity **/
class Group
{
// ...
/**
* #ManyToMany(targetEntity="User", mappedBy="groups")
* #JoinTable(name="users_groups")
**/
private $users;
// ...
}
This, however, returns the following error when I try to update the tables:
[Doctrine\DBAL\Schema\SchemaException]
The table with name 'postgres.user_groups' already exists.
How do I create a many-to-many relationship that shares the same table 'user_groups'?
Note: I understand that I can remove the #JoinTable(name="users_groups") but when I do this I no longer have a Many-to-Many relationship with two owning sides. Instead only one side (owning side) knows about the join table.

Remove #JoinTable(name="users_groups") annotation from your inverse side entity that is Group, Once owning side entity has mapping information then there is no need to define again in inverse side entity, some of the key point related to your question
The inverse side has to use the mappedBy attribute of the OneToOne,
OneToMany, or ManyToMany mapping declaration. The mappedBy attribute
contains the name of the association-field on the owning side.
The
owning side has to use the inversedBy attribute of the OneToOne,
ManyToOne, or ManyToMany mapping declaration. The inversedBy attribute
contains the name of the association-field on the inverse-side.
You can pick the owning side of a many-to-many association yourself
Reference Bidirectional Associations
class Group
{
/**
* #ManyToMany(targetEntity="User", mappedBy="groups")
**/
private $users;
}
See Many-To-Many, Bidirectional
example from documentation

Related

Symfony 4 how to delete entity from OneToMany relationship

I'm having a bit of a problem deleting an entity assigned to another with a OneToMany relationship.
I have an entity called Business and it has a property "units" which is a collection of Unit entities on a OneToMany relationship (business can have many units).
When i try to delete a single unit from the database i get a violation of the foreign keys, it won't let me remove the unit from the business entity.
Here is a condensed version of both entities:
BUSINESS
/**
* #ORM\Entity(repositoryClass="App\Repository\BusinessRepository")
*/
class Business
{
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="App\Entity\Unit", mappedBy="business")
*/
private $units;
}
UNIT
/**
* #ORM\Entity(repositoryClass="App\Repository\UnitRepository")
*/
class Unit
{
/**
* #var Business
* #ORM\ManyToOne(targetEntity="App\Entity\Business", inversedBy="units")
* #ORM\JoinColumn(name="business_id", referencedColumnName="id")
*/
private $business;
}
So in the UnitRepository i have a delete method:
/**
* #param Unit $unit
*/
public function delete(Unit $unit){
$this->em->remove($unit);
$this->em->flush();
}
And i get this error:
An exception occurred while executing 'DELETE FROM unit WHERE id = ?' with params [1]:
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`businessdirectory`.`unit_day`, CONSTRAINT `FK_F03D80CEF8BD700D` FOREIGN KEY (`unit_id`) REFERENCES `unit` (`id`))
I don't know if i have set up the relationship incorrectly or not here, but i should be able to delete a single unit from a business, and i should be able to delete the entire business with it's units.
See if a Unit entity is the owning side of another relationship. At that point you would need to delete all the entities that depend on Unit first. You can freely delete the owned side of a One-To-Many relationship but you would need to clear all owned elements before deleting the owning side.

Symfony2 misconfigured entity

I am getting two warnings about misconfigured entities in my Symfony 2 project. It runs fine in the development environment, but the production environment will not start and I suspect these misconfigured entities might be the reason.
It is the same error on both entities so I am only including one of them as an example.
BizTV\MediaManagementBundle\Entity\QrImage:
The field BizTV\MediaManagementBundle\Entity\QrImage#visits is on the inverse side of a bi-directional relationship, but the specified mappedBy association on the target-entity BizTV\MediaManagementBundle\Entity\QrVisit#QrImage does not contain the required 'inversedBy="visits"' attribute.
QrVisit entity:
class QrVisit
{
...
/**
* #var object BizTV\MediaManagementBundle\Entity\QrImage
*
* #ORM\ManyToOne(targetEntity="BizTV\MediaManagementBundle\Entity\QrImage")
* #ORM\JoinColumn(name="QrImage", referencedColumnName="id")
*/
protected $QrImage;
QrImage entity:
class QrImage
{
...
/**
* #ORM\OneToMany(targetEntity="BizTV\MediaManagementBundle\Entity\QrVisit", mappedBy="QrImage")
*/
private $visits;
I changed QrImage to include the inversedBy as below, but I probably did it wrong because I still get an error message, although a new one.
/**
* #ORM\OneToMany(targetEntity="BizTV\MediaManagementBundle\Entity\QrVisit", mappedBy="QrImage", inversedBy="visits")
*/
private $visits;
But this generates the error:
[Creation Error] The annotation #ORM\JoinColumn declared on property BizTV\UserBundle\Entity\UserGroup::$company does not have a property named "inversedBy". Available properties: name, referencedColumnName, unique, nullable, onDelete, columnDefinition, fieldName
If you want to establish a bi-directional ManyToOne / OneToMany relationship you'll have to put the mappedBy attribute on the OneToMany side like:
#ORM\OneToMany(targetEntity="BizTV\MediaManagementBundle\Entity\QrVisit", mappedBy="QrImage")
and the inversedBy on the ManyToOne side like:
#ORM\ManyToOne(targetEntity="BizTV\MediaManagementBundle\Entity\QrImage", inversedBy="visits")
that's all you need here. For your reference please check Doctrine doc
The error that you're getting refers to a different entity (UserGroup) but you can check them in the same fashion.

symfony2: How to remove related entity with doctrine / restrict annotation?

I have some bit of troubles with delete constraint in an entity.
I have an entity merchandise and an entity vehicle with a relation many to one in merchandise, so a merchandise only could be in one vehicle, and a vehicle could have many merchandise. So I have:
class Merchandise{
/**
* #ORM\ManyToOne(targetEntity="Vehicle",inversedBy="merchandise")
* #ORM\JoinColumn(name="vehicle", referencedColumnName="id")
*/
private $vehicle;
}
class Vehicle{
/**
* #ORM\OneToMany(targetEntity="Merchandise",mappedBy="vehicle")
*/
private $merchandise;
}
What I want to get is that when I try to delete a Merchandise which have a vehicle, the Merchandise couldn't be deleted.
But I don't know how can I put an ORM Level restrict constraint. I tried restrict={"remove"} but it doesn't exist in #ORM\OneToMany.
I also try to put a preRemove function which return false, but it doesn't work :(
Any idea?
Thanks!!!
ManyToOne / inversedBy is the OWNING side of the bidirectional relation from doctrine's point of view - which can lead to confusion.
To resolve your issue add cascade operation to your merchandise entity. example:
/**
* #ORM\ManyToOne(targetEntity="Vehicle",mappedBy="merchandise", cascade={"all"})
*/
cascade can be set to a combination of :
persist
remove
merge
detach
all
Improve further by adding cascade ( ORM-level ) to your Vehicle entity aswell. example:
/**
* #ORM\OneToMany(targetEntity="Merchandise", mappedBy="vehicle", cascade={"persist","remove"})
*/
... or use onDelete ( database-level ) with one of
SET NULL
CASCADE
... like this
/**
* #ORM\OneToMany(targetEntity="Merchandise", inversedBy="vehicle", onDelete="CASCADE")
*/
Now if you remove a Vehicle - the related Merchandise entities will be removed. Added Merchandises will automatically be saved.
... finally update your schema and drop -> re-create your database if constraints have not been updated and errors occur. Make sure both sides use the cascade option.
Read more in the documentation chapter Transitive persistence / Cascade Operations.

Composite key and form

I have the following associations in my database (simplified version):
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.
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.
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.
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Ă !

doctrine2: undefined index - many-to-one with non default referencedColumnName does not persist entity

I'm using Symfony 2.1.2.
I have two entities and define a [many-to-one (bidirectional)] (1) association between them. I don't want to use the primary key for the foreign key (referencedColumnName). I want to use another integer unique column: customer_no
/**
* #ORM\Entity
* #ORM\Table(name="t_myuser")
*/
class MyUser extends BaseEntity // provides an id (pk)
{
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="user")
* #ORM\JoinColumn(name="customer_no", referencedColumnName="customer_no", nullable=false)
*/
public $customer;
}
/**
* #ORM\Entity
* #ORM\Table(name="t_customer")
*/
class Customer extends BaseEntity // provides an id (pk)
{
/**
* #ORM\Column(type="integer", unique=true, nullable=false)
*/
public $customer_no;
/**
* #ORM\OneToMany(targetEntity="MyUser", mappedBy="customer")
*/
public $user;
}
When I try to persist a MyUser entity with an Customer entity, I get this error:
Notice: Undefined index: customer_no in ...\vendor\doctrine\orm\lib\Doctrine\ORM\Persisters\BasicEntityPersister.php line 608
The schema on the db looks fine, these should be the important sql schema definitions:
CREATE UNIQUE INDEX UNIQ_B4905AC83CDDA96E ON t_customer (customer_no);
CREATE INDEX IDX_BB041B3B3CDDA96E ON t_myuser (customer_no);
ALTER TABLE t_myuser ADD CONSTRAINT FK_BB041B3B3CDDA96E FOREIGN KEY (customer_no)
REFERENCES t_customer (customer_no) NOT DEFERRABLE INITIALLY IMMEDIATE;
So there is definitely an index for customer_no
//update:
I fix the inversedBy and mappedBy stuff, but this is not the problem.
(1) : http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-many-bidirectional
#m2mdas:
Yes you're right, I thought it's possible because JPA (which has influence to doctrine) has this feature. The attribute referencedColumnName only for the case when your property does not match the table column.
Whatever, I found a solution by patching the BasicEntityPersister.php, see here the gist on github: https://gist.github.com/3800132
the solution is to add the property/field name and value for the mapping column. This information is already there but not bound to the right place. It have to be added to the $newValId arrray this way:
$fieldName = $targetClass->getFieldName($targetColumn);
$newValId[$fieldName] = $targetClass->getFieldValue($newVal, $fieldName);
It only works for ManyToOne reference. ManyToMany doesn't work.
For ManyToOne I test it with already existing entities. You can test it, too:
change the doctrine annotation in tests/Doctrine/Tests/Models/Legacy/LegacyArticle.php
from
#JoinColumn(name="iUserId", referencedColumnName="iUserId")
to
#JoinColumn(name="username", referencedColumnName="sUsername")

Resources