Symfony 4 how to delete entity from OneToMany relationship - symfony

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.

Related

On delete of Parent entities - null the association on the other entity

I am trying to solve my issue on Doctrine ORM. I have 2 parent entities: CompanyDoctrineEntity and ServiceDoctrineEntity and 1 entity that are associated with these 2 (but the association is not required) OrderLinkRedirectLogDoctrineEntity. The association in OrderLinkRedirectLogDoctrineEntity is defined by:
class OrderLinkRedirectLogDoctrineEntity {
/**
* #Id
* #Column(type="integer")
* #ORM\GeneratedValue()
*
* #var int $id
*/
private $id;
/**
* Many logs have one company. This is the owning side.
*
* #ManyToOne(targetEntity="CompanyDoctrineEntity", cascade="detach")
* #JoinColumn(name="company_id", referencedColumnName="id")
*
* #var CompanyDoctrineEntity $company
*/
private $company;
/**
* Many logs have one service. This is the owning side.
*
* #ManyToOne(targetEntity="ServiceDoctrineEntity", cascade="detach")
* #JoinColumn(name="service_id", referencedColumnName="id")
*
* #var ServiceDoctrineEntity $service
*/
private $service;
}
My expected behaviour is, whenever either CompanyDoctrineEntity or ServiceDoctrineEntity is removed from the database, the association in the OrderLinkRedirectLogDoctrineEntity will be NULLed, which I believe what the cascade="detach" does, but for some reason, it's not working, as I am getting the following errors:
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`test_app2`.`logs_order_link_redirects`, CONSTRAINT `FK_6C1CA74CED5CA9E6` FOREIGN KEY (`service_id`) REFERENCES `app_services` (`id`)) in /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:117
Stack trace:
#0 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php(117): PDOStatement->execute(NULL)
#1 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php(1054): Doctrine\DBAL\Driver\PDOStatement->execute()
#2 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php(656): Doctrine\DBAL\Connection->exe in /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php on line 49
I'm far from an expert on Doctrine, so take this with a grain of salt and test thoroughly.
Your relations are not nullable (defaults to false), which is why your foreign key constraint is complaining: logs_order_link_redirects.service_id (and company_id) isn't allowed to be null. That likely wasn't a problem before because you're not inserting the OrderLinkRedirectLogDoctrineEntity entities without the relationships. If you were to say
$redirectLog = new OrderLinkRedirectLogDoctrineEntity();
$entityManager->persist($redirectLog);
$entityManager->flush();
you'd probably trigger the same error immediately.
Also, I don't believe you want cascade={"detach"} here. Detach would just remove the entity from this entity manager instance (in other words: for the running process), so anything you'd do to the entity after detaching it wouldn't be reflected in the database when $entityManager->flush() is called. On the next request, the entity would be back in the entity manager.
I believe that adding nullable=true to your ManyToOne's JoinColumn annotations, e.g.
#JoinColumn(name="company_id", referencedColumnName="id", nullable=true)
will get you the result you're looking for. You'll need to update your database schema afterwards for changes to be applied to the tables. Also, make sure you don't have (or add) orphanRemoval=true on the inverse side so Doctrine doesn't automatically remove your entities if they lose their parent.
I prefer adding the JoinColumn annotation to relationships as well, even though it's not required if you're fine with Doctrine's default field name choices. Adding nullable=false makes it more explicit that this relationship cannot be null. That's implied if you don't have nullable=true, but when I start looking at relationships and need to know whether they can be null or not, I'm usually confused by something and I don't have mental energy to spare to actively remember the default values for important attributes.

doctrine nested entities cascade persist : how to reuse existing entities

If entity A contains multiple entity B and has cascade:persist, how to reuse existing entities B when persisting ?
B entity has one primary key, an integer, and the id of the A parent. The only data it contains is the primary key.
Example:
A has 2 B entities, identified by their id, 14 and 23.
A.Bs = [{id=14, AId=A.id}, {id=23, AId=A.Id}]
Now if I modify this managed entity, to add a B entity to A, with id = 56.
A.Bs = [{id=14, AId=A.id}, {id=23, AId=A.Id}, {id=56}]
Relationships
Entity A
/**
* #var B[]|ArrayCollection
*
* #ORM\OneToMany(targetEntity="B", mappedBy="A", cascade={"persist", "remove"}, orphanRemoval=true)
* #Assert\Valid
*/
private $Bs;
Entity B
/**
* #var A
*
* #ORM\ManyToOne(targetEntity="A", inversedBy="Bs")
* #ORM\JoinColumn(name="A_id", referencedColumnName="A_id")
* #Assert\NotNull()
*/
private $A;
If I try to persist I get Integrity constraint violation, because Doctrine tries to persist the existing entities, that have id 14 and 23.
I understand this is expected behaviour, but how can I make it persist new entities, and reuse existing ones ?
More details:
If I get an existing entity A with $em->find($id) and directly use persist and flush, I will get UniqueConstraintException because it tries to persist the already persisted B entities.
Example code:
/** #var A $existingEntityA */
$existingEntityA = $this->getEntity($id);
$this->serializerFactory->getComplexEntityDeserializer()->deserialize(json_encode($editedEntityADataJson), A::class, 'json', ['object_to_populate' => $existingEntityA]);
$this->entityValidator->validateEntity($existingEntityA);
$this->_em->flush();
Example error : Integrity constraint violation: 1062 Duplicate entry '777111' for key 'PRIMARY'
If I understand your example properly - you're doing something like this:
$b = new B();
$b->setId(56);
$a->getB()->add($b);
and you having a row with primary key 56 into database table that is represented by B?
If my assumption is correct - it is wrong way to go. Reason is that Doctrine internally stores so called "identity map" that keeps track of all entities that either being fetched from database or persisted by calling EntityManager::persist(). Every entity that is scheduled for commit but not available into identity map is considered as "new" and scheduled for insertion. If row with same primary key is already available in database - you're receiving UniqueConstraintException.
Doctrine doesn't handle a case "let me look if there is an entity with such primary key in database" by itself because it will hurt performance significantly and is not needed in most cases. Each such test will result into database query, imagine if you will have thousands of such entities. Since Doctrine doesn't know business logic of your application - it will spend even more resources with attempts to guess optimal strategy so this is intentionally left out of scope.
Correct way for you would be to get your entity by itself before adding to collection:
$newB = $em->find(B::class, 56);
if ($newB) {
$a->getB()->add($newB);
}
In this case new entity will internally have "managed" status and will be correctly handled by Doctrine at a time of commit.

Symfony2 Many-to-Many relationship sharing one JoinTable

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

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.

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