I'm trying to understand the cascade option in Doctrine in Symfony2.
I would like to be able to delete a child entity (and not trigger the foreign key constraint error.)
I have 3 entities:
Report
/**
* #ORM\OneToMany(targetEntity="Response", mappedBy="report")
*/
protected $responses;
/**
* #ORM\OneToMany(targetEntity="Response", mappedBy="report")
*/
protected $sms;
Response
/**
* #ORM\ManyToOne(targetEntity="Report", inversedBy="responses")
*/
protected $report;
SMS
/**
* #ORM\ManyToOne(targetEntity="Report")
*/
protected $report;
Now I would like to delete a Response entity but I get
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row:
a foreign key constraint fails (mybundle.sms, CONSTRAINT FK_B0A93A77BB333E0D FOREIGN KEY (reportId) REFERENCES report (id))
Where do I use the cascade option and which option should I use (detach or remove)?
I can do a lot of trial and error to figure this out, but I was hoping for an expert explanation, so I don't overlook something.
Try using
/**
* #ORM\ManyToOne(targetEntity="Report", inversedBy="responses")
* #ORM\JoinColumn(name="reportId", referencedColumnName="id", onDelete="CASCADE")
*/
protected $report;
And then update yor schema. It will add database level Cascading
Ziumin's answer
using the onDelete option for the ORM JoinColumn
method worked when you want to delete a child item (Owning Side).
But if you want to delete a Response which is a parent item (Inverse Side), this is when cascade comes in handy. In the Report entity I added the following for each of its collections (OneToMany relationships):
Report
/**
* #ORM\OneToMany(targetEntity="Response", mappedBy="report", cascade={"remove"})
*/
protected $responses;
/**
* #ORM\OneToMany(targetEntity="SMS", mappedBy="report", cascade={"remove"})
*/
protected $sms;
Now, when I delete a Report, it removes all of its associated entries in the Response and SMS tables.
You may also use cascade=all for update all actions.
Report
/**
* #ORM\OneToMany(targetEntity="Response", mappedBy="report", cascade={"all"})
*/
protected $responses;
/**
* #ORM\OneToMany(targetEntity="SMS", mappedBy="report", cascade={"all"})
*/
protected $sms;
Related
I setup a pretty straightforward Group -> GroupUser -> User relationship (GroupUser being a join table with additional data fields, Group OneToMany GroupUsers, which has ManyToOne Group and User, and User with OneToMany GroupUser) and fail to POST to the API, receiving an invalid IRI error. There seems to be quite some confusion about this, I found several posts (here and here) on this but none clearly addresses my most basic scenario (I followed the API platform documentation and have a Symfony4 with a require api-platform/api-pack installation).
I am using IRI to associate the objects, but the following POST does not work (I do have a user with id 1 and a group with id 1) and returns the Invalid value provided (invalid IRI?) error:
curl -X POST "http://api.platform.local/group_users" -H "accept: application/ld+json" -H "Content-Type: application/ld+json" -d "{ \"group\": \"/groups/1\", \"user\": \"/users/1\", \"role\": \"member\"}"
Here are my entities:
App/Entity/Group
/**
* #ORM\OneToMany(targetEntity="GroupUser", mappedBy="group")
*/
private $users;
App/Entity/User
/**
* #ORM\OneToMany(targetEntity="GroupUser", mappedBy="user")
*/
private $groups;
App/Entity/GroupUser
/**
* #Assert\NotBlank
* #ORM\JoinColumn(
* nullable=false,
* onDelete="CASCADE"
* )
* #ORM\ManyToOne(
* inversedBy="users",
* targetEntity="App\Entity\Group"
* )
*/
private $group;
/**
* #Assert\NotBlank
* #ORM\JoinColumn(
* nullable=false,
* onDelete="CASCADE"
* )
* #ORM\ManyToOne(
* inversedBy="groups",
* targetEntity="App\Entity\User"
* )
*/
private $user;
I must be missing something fundamental, but the documentation doesn't seem to require anything else. This post didn't help, I even added an id to the UserGroup entity just for that, initially I only had user and group as table ids.
Your help is appreciated, many thanks beforehand.
I finally figured this out, actually I made two mistakes:
I passed values to __construct to populate a GroupUser upon construction. I removed the __construct entirely, this didn't play nice with the API Platform and the automatic IRI creation.
I used a composite primary key on group_id and user_id for my GroupUser table, which I abandoned in favour of an own ID.
Pretty straight forward, here's what I added to the GroupUser entity:
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Id
*/
private $id;
And don't forget the getter method:
public function getId(): ?int
{
return $this->id;
}
In an application I have a case of the Class Table Inheritance. The discriminator column is an ENUM:
/**
* Foo
*
* #ORM\Table(name="foos", ...)
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="`type`", type="string", columnDefinition="ENUM('bar', 'buz')")
* #ORM\DiscriminatorMap({
* "bar" = "Bar",
* "buz" = "Buz"
* })
*/
abstract class Foo
{
...
}
Doctrine works as expected (to begin with). The doctrine:migrations:diff command creates a migration for the tables and relationships and also defines the discriminator column correctly, as an ENUM.
Then I execute the migrations (doctrine:migrations:migrate). The schema looks well. But:
When I execute the diff command again (and expect no new migrations), I get a new migration generated:
final class Version20180619205625 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->addSql('ALTER TABLE foos CHANGE type `type` ENUM(\'bar\', \'buz\')');
}
public function down(Schema $schema) : void
{
$this->addSql('ALTER TABLE tasks CHANGE `type` type VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci');
}
}
Alright, I execute it. And try the diff command again. And get the same migration generated again... So, Doctrine seems to "think", the column is still VARCHAR.
I showed the issue here on example of an inheritance discriminator. But actually it doesn't matter, if the column is a discriminator or not -- this behavior is the same for every ENUM column.
How to solve this issue? Is there a way make Doctrine handle ENUM columns correctly?
I want to add a getHomeAddress() to a customer entity in doctrine using symfony wondering what the best practice would be
Customer Entity
/**
* #ORM\ManyToMany(targetEntity="Addresses", inversedBy="customers")
* #ORM\JoinTable(name="Customer_Addresses")
*/
private $addresses;
Addresses Entity
/**
* #ORM\ManyToMany(targetEntity="Customers", mappedBy="addresses")
*/
private $customers;
Customers Entity
$customers->getHomeAddress();
This codes for in entity mapping annotation, more information this address Doctrine Association Mapping http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html
I've been trying to figure out how to get this to work for along time but without any luck. Due to a complex logic in an app I'm working on, I need to create an isolated clone of a entity collection without preserving what so ever relation to the database. Whatever changes I do on the cloned collection should not be tracked by Doctrine at all and should be treated as if it doesn't exist at all.
Here's an example code:
/*
* #ORM\Entity()
*/
class Person
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="person_id", type="integer",nullable=false)
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Car", mappedBy="person", cascade={"persist"})
*/
public $cars;
}
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Car
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="car_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/*
* #ORM\JoinColumn(name="person_id", referencedColumnName="person_id", nullable=true)
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars", cascade={"persist"})
*/
private $person;
}
I've already tried the following code in my controller to store the collection into the session but it still somehow stores the relationships:
$tmp = clone $person;
$this->get('session')->set('carCollection', $tmp->getCars());
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$tmpCollection->add(clone $car);
}
$this->get('session')->set('carCollection', $tmpCollection);
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$clone = clone $car;
$entityManager->detach($car);
$tmpCollection->add(clone $clone);
}
$this->get('session')->set('carCollection', $tmpCollection);
Apparently I'm doing something wrong here because I end up having more results in the Car collection when flushing the entity even though the collection itself has the correct number of records. I have a suspicion that somewhere in the chain Doctrine doesn't compute correctly what needs to be done.
Any ideas or directions on how to solve or debug this?
Follow-up question: When retrieving back the cloned collection from the session will it still be an isolated clone or Doctrine will try merge it back?
I'm writing this answer to give directions to anybody who might have similar issues. I couldn't find many topics or documentation in this manner which is why I decided to share my experience. I am no deep expert on Doctrine an how it internally works, so I won't go into big details of "how it works". I will rather focus on the end result.
Storing entities which have relations to other entities into a session is quite problematic. When you retrieve it from the session, Doctrine loses track of the relationships (OneToMany, ManyToOne, etc). This leads to some undesired effects:
Doctrine wrongly decides to insert a new record of an existing entity.
Doctrine might throw exceptions such as A new entity was found through the relationship 'Acme\MyBundle\Entity\Person#cars' that was not configured to cascade persist operations for entity: Opel. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). and at least 2 other types of exceptions which might seem totally irrelevant at first.
Apparently when fetching a result from the database and it "as-is" in your session things get really messy, specially if the entity has relations to other entities (which was my case). Pay big attention if you have entity relationships - they might need to be "refreshed" if you start getting strange exceptions.
There are a couple of ways to overcome this issue. One of which is to use the data sent via the form (as #flec suggested) by using $myForm->getData(). This approach might work well for you, but unfortunately it was not the case with me (too complex to explain).
What I ended up doing was implementing the \Serializable in the entity. I also created a method called __toArray() which converted my entity into an array. What data you return in the __toArray() method is totally up to you and your business logic. The array data is stored into the session and you use it to re-create a fresh object with all necessary relations.
Hope this helps somebody.
I think hydrators/extractors would be the way to go for you.
They can extract the data from an entity and you can pass them to a newly created instance of that entity via the hydrator.
The only thing you'll need to do in between is the unsetting of the relation properties.
They should be fetchable via a metadata class via doctrine somehow.
class Account
{
...
/*
* #OneToMany(targetEntity="Address", mappedBy="account");
*/
private $addresses;
...
}
class Address
{
...
/**
* #ManyToOne(targetEntity="Account", inversedBy="addresses")
* #JoinColumn(name="account_id", referencedColumnName="id")
*/
private $account;
...
}
When i run console command to validate schema:
[Mapping] FAIL - The entity-class 'Entity\Address' mapping is invalid:
* The association Entity\Address#account refers to the inverse side
field Entity\Account#addresses which does not exist
Why?
I think the Doctrine annotation reader uses php's Reflection API, specifically getDocComment to read the annotations. That means your doc block comments must begin with /** otherwise they will be ignored. Your $addresses property annotation begins with /* so it won't get picked up.