I have made a really basic example with two models.
"Singer" extends from "Person"
I am using class table Inheritance with these two models:
<?php
namespace models;
/**
* #Table(name="persons")
* #entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({"singer" = "\models\Singer"})
*/
abstract class Person {
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $name;
The singer model looks like:
namespace models;
/**
* #Table(name="singers")
* #entity
*/
class Singer extends Person{
/** #column(type="string", nullable=false) */
protected $gender;
}
Workflow
Consider this scenario.
In the db I have these rows:
persons table:
id_person | name | type
-----------------------
1 john singer
singers table:
id_person | gender
------------------
1 pop
I proceed to remove this singer:
$singer = $em->find('models\Singer', 1);
$em->remove($singer);
$em->flush();
After execute the code above, I check again the database and I found this:
persons table:
id_person | name | type
-----------------------
(empty)
singers table:
id_person | gender
------------------
1 pop
As you note, the row from child table was not removed as expected.
So, after searching in doctrine's documentation, it states:
When you do not use the SchemaTool to generate the required SQL you should know that deleting a class table inheritance makes use of the foreign key property ON DELETE CASCADE in all database implementations. A failure to implement this yourself will lead to dead rows in the database.
So, I proceed to alter persons table as below:
ALTER TABLE persons
ADD CONSTRAINT fk_persons_1
FOREIGN KEY (id_person)
REFERENCES singers (id_person)
ON DELETE CASCADE
ON UPDATE NO ACTION;
Now, the problems get complicated:
when I remove a singer, the information still there, even the persons table was altered in order to delete from singers table too.
When I try to insert a new singer like
$singer = new \models\Singer('Zara', 'rock');
$em->persist($singer);
$em->flush();
it throws an exception:
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`practica`.`persons`, CONSTRAINT `fk_persons_1` FOREIGN KEY (`id_person`) REFERENCES `singers` (`id_person`) ON DELETE CASCADE ON UPDATE NO ACTION)' in /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php:138 Stack trace: #0 /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php(138): PDOStatement->execute(NULL) #1 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php(165): Doctrine\DBAL\Statement->execute() #2 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(929): Doctrine\ORM\Persisters\JoinedSubclassPersister->executeInserts() #3 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(318): Doctrine\ORM\UnitOfWork->executeInserts(Object(Doctrine\ORM\Mapping\ClassMetadata)) #4 /var/www/html/doctrine/vendor/doct in /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php on line 47
So, basically, all I need is to remove the information in child table too in MySQL db. But I do not get it.
Try reversing the FOREIGN KEY i.e. remove it from your persons table and add it to your singers table
ALTER TABLE singers
ADD CONSTRAINT fk_singers_1
FOREIGN KEY (id_person)
REFERENCES persons (id_person)
ON DELETE CASCADE
ON UPDATE NO ACTION;
I think you probably have to ask yourself if you really need a field relating to multiple tables.
In any case, maybe is better to do it through a relation instead of using inheritance and a discriminator map, that works well with objects using the same table, but since in your case they are different tables it is probably better to use a relation instead:
<?php
namespace models;
/**
* #Table(name="persons")
* #entity
*/
class Person {
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $name;
/**
* #OneToOne(targetEntity="Singer")
* #JoinColumn(name="id_person", referencedColumnName="id_person")
*/
private $singer;
namespace models;
/**
* #Table(name="singers")
* #entity
*/
class Singer{
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $gender;
/**
* #OneToOne(targetEntity="Person", cascade={"persist", "remove", "merge"}, orphanRemoval=true)
* #JoinColumn(name="id_person", referencedColumnName="id_person")
*/
private $person;
}
By trying to attach the design to the real world you are over complicating the implementation which will make your application hard to mantain.
I did not try the code (probably needs adjustement).
Related
i have two entities : Post and Comments where a Post can have one or more comments.
I implemented a cascade={"remove"} on the Post's comments OneToMany which works greats !
All comments related to a Posts gets deleted after Post suppression.
Then, i decided to add a topComment attribute in my Post Entity (Many topComment to One Post). After that, i tried to delete a Post with related Comments but the suppression fails because of the foreign key constraint even if the post have no topComment...
Here is the error i get :
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`lab_test_cascade_remove`.`comment`, CONSTRAINT `FK_9474526C4B89032C` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`))
I got rid of this error by adding a #JoinColumn(nullable=true) annotation to the Post's topComment attribute but i find that strange because this is the default behavior of doctrine if #JoinColumn annotation isn't set...
Could someone can please explain me what happens internally and cause this error even if the Post don't have any topComment ?
Many thanks in advance.
Here are my entities (attributes only) :
Post :
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="post", cascade={"remove"})
*/
private $comments;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Comment")
*/
private $topComment;
Comment :
/**
* #ORM\Entity(repositoryClass="App\Repository\CommentRepository")
*/
class Comment
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Post", inversedBy="comments")
* #ORM\JoinColumn(nullable=false)
*/
private $post;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
I want to delete a property with my user but I have the following error :
An exception occurred while executing 'DELETE FROM property WHERE id = ?' with params [1]:
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (gestImmo.equipment, CONSTRAINT FK_D338D583517FE9FE FOREIGN KEY (equipment_id) REFERENCES property (id))
I googled it and (I think) it's a cascade problem. So I searched on forums but I didn't solved the problem. I asked for help to an experimented coworker but we didn't fix the mistake ... Hope you could help me.
In my User entity there is :
/**
* #ORM\OneToMany(targetEntity="App\Entity\Property", mappedBy="userProperty")
*/
private $properties;
In my property Entity there is :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="properties")
* #JoinColumn(name="id", referencedColumnName="id", onDelete="CASCADE")
*/
private $userProperty;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Equipment", mappedBy="equipment")
* #JoinColumn(name="id", referencedColumnName="equipement_id", onDelete="CASCADE")
*/
private $equipments;
and in my equipments entity there is :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Property", inversedBy="equipments")
* #Assert\Type("string")
* #var string
*/
private $equipment;
Thanks for your help !
Setting the onDelete="CASCADE" in your manyToOne annotation should be enough.
Don't forget to bin/console make:migration after that. Everything you change in your ORM must be migrated in order to apply the changes into the database.
You cannot delete property because it is referenced in column equipment_id in table equipment. This foreign key constraint means that equipment.equipment_id must point to a valid(existing) id of property - property.id.
If you want to delete property, then before that you either:
Delete equipment records where equipment_id = {id of property you want to delete}
Or change those equipment_id to null
EDIT
Looks like your annotations are incorrect. If I understood your relations correctly, then it should be something like this.
Property entity:
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="properties")
* #JoinColumn(name="user_property_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $userProperty;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Equipment", mappedBy="equipment")
*/
private $equipments;
Equipment entity:
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Property", inversedBy="equipments")
* #ORM\JoinColumn(name="equipment_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $equipment;
I have an issue with the following one-to-many unidirectional relation between UserSociable and Credential (I have simplified the code a little bit).
UserSociable entity is the owner of the relation:
/**
* Class UserSociable
* #ORM\Entity(repositoryClass="Repository\UserSociableRepository")
* #ORM\Table(name="userSociable")
* #DiscriminatorEntry(value="userSociable")
*/
class UserSociable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Credential")
* #ORM\JoinTable (
* joinColumns={#ORM\JoinColumn(name="entity_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="credential_id", referencedColumnName="id", unique=true)}
* )
*/
private $credentials;
}
Credential entity is the inverse side:
/**
* #ORM\Entity(repositoryClass="Repository\CredentialRepository")
* #ORM\Table(name="credentials")
*/
class Credential
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
}
These creates three tables in DB:
userSociable
credentials
usersociable_credential
Once the DB is populated, I have the following issue: when I delete a userSociable, Doctrine automatically also removes the corresponding rows in the join table usersociable_credential.
I was expecting an exception to raise since the PK is referenced as FK in usersociable_credential table. This is the expected behaviour since I never used an annotation like onDelete="CASCADE" in the join column annotation.
The thing is I want to force to delete all the user's Credentials before to being able to delete an UserSociable.
I have looked at MySQL log file and found the following queries:
151106 12:16:24 113 Query START TRANSACTION
113 Query DELETE FROM usersociable_credential WHERE entity_id = '1'
113 Query DELETE FROM ku_user WHERE id = '1'
113 Query commit
113 Quit
112 Quit
How can I avoid the query DELETE FROM usersociable_credential WHERE entity_id = '1' to be created and executed by Doctrine???
Thanks in advance.
I am trying to construct an object with two composite foreign keys pointing out to the same object, but they seem to have the same data, like doing the join only on one column, product_id.
class PostpaidProduct extends Product {
/**
* #ManyToOne(targetEntity="Bundle", fetch="EAGER", cascade={"persist"})
* #JoinColumn(name="bundle_voice_id", referencedColumnName="id")
*/
private $bundleVoice;
/**
* #ManyToOne(targetEntity="Bundle", fetch="EAGER", cascade={"persist"})
* #JoinColumn(name="bundle_data_id", referencedColumnName="id")
*/
private $bundleData;
/**
* #OneToMany(targetEntity="BundlePromo", mappedBy="product", fetch="EAGER", cascade={"persist"})
* #JoinColumns({
* #JoinColumn(name="id", referencedColumnName="product_id"),
* #JoinColumn(name="bundle_voice_id", referencedColumnName="bundle_id")
* })
*/
private $bundleVoicePromos;
/**
* #OneToMany(targetEntity="BundlePromo", mappedBy="product", fetch="EAGER", cascade={"persist"})
* #JoinColumns({
* #JoinColumn(name="id", referencedColumnName="product_id"),
* #JoinColumn(name="bundle_data_id", referencedColumnName="bundle_id")
* })
*/
private $bundleDataPromos;
}
What would be wrong with my mapping?
Is it possible to have composite foreign keys but without being primary keys?
I have talked to one of the developers of Doctrine and he said that the #JoinColumns field in #OneToMany relationships is ignored. The alternative would be having just one foreign key and to use matching criterias in an entity method, filtering for the entries needed based on the other key. Another solution would be having repository methods specific for getting these values.
Also, in OneToMany relationships eager fetching does not work, so it does separate queries for all children. So if you have a product with multiple prices, when fetching a product it will do separate queries for fetching the prices.
I need to map the same column to 2 differences tables (lets say normal and extended).
/**
* #var ItemValue
*
* #OneToOne(targetEntity="ItemValue")
* #JoinColumn(name="id_value", referencedColumnName="id_value")
*/
private $value;
/**
* #var ItemValueExtended
*
* #OneToOne(targetEntity="ItemValueExtended")
* #JoinColumn(name="id_value", referencedColumnName="id_value")
*/
private $valueExtended;
/**
* #var string $isExtended
*
* #Column(name="is_extended", type="string", nullable=false)
*/
private $isExtended = 'YES';
I have no problem with joining data based on the isExtended attribute using DQL:
"SELECT id,idv FROM ItemData id
JOIN id.value idv WHERE id.isExtended='NO'";
and
"SELECT id,idv FROM ItemData id
JOIN id.valueExtended idv WHERE id.isExtended='YES'";
but when ever I want to persist a new object, NULL is inserted in id_value column ?!!
$oValue = ItemValue();
.
.
$oData = new ItemData();
$oData->setValue($oValue);
.
.
.
$em->persist($oData);
$em->flush();
Any Idea ?
From Doctrine2 documentation:
In the case of bi-directional associations you have to update the
fields on both sides.
One possible solution would be:
$oData = new ItemData();
$oData->setValue($oValue);
$oValue->setData($oData);
but it's tedious. Another better one is set the cascade option on both sides of the one-to-one association:
#OneToOne(targetEntity="ItemValue"), cascade={"persist", "remove"})
This way your code will work. You can choose the appropriate cascade options looking at here.
In the case where both the parent and child entity are new entities (neither have been persisted) a PrePersist lifecycle event on the parent can help:
/**
* ....
*
* #ORM\HasLifecycleCallbacks
*/
class ParentEntity {...
/**
* #ORM\PrePersist()
*/
public function prePersist() {
foreach($this->getChildEntities() as $childEntity) {
$childEntity->setParent($this);
}
}
-
class ChildEntity {
....
This will automatically create the child -> parent relationship upon saving the parent.
In many instances Doctrine will then be able to work out the rest at the SQL level.