Delete associated/referenced entity - doctrine-orm

I have a table product, the items in these table are referenced in tables such as cart_item and order_item as well as shipping_item etc.
All these references are optional (the product_id is set to nullable in those tables).
I need to have a way to delete a product and still keeping the other tables's records. One way I can think of is to go into all those tables, set the product_id to null, then go back to the product table to delete. However, since I may not know all the tables that are referencing to product (many other bundles can have entities that are referencing to this product), is there a way that I can know all these association to loop through and set null?
(Or perhaps there is a better way?)
PS: the idea that this is a shopping cart and the owner may want to remove expired products to clean up but for ordered, shipped items they still need to keep records.
Edit1:
This is the definition of the product reference in the OrderItem entity:
/**
* #var \Product
*
* #ORM\ManyToOne(targetEntity="Product")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
* })
*/
private $product;
The error I'm getting:
PDOException: SQLSTATE[23000]: Integrity constraint violation: 1451
Cannot delete or update a parent row: a foreign key constraint fails
(test.order_item, C ONSTRAINT fk_order_item_product1 FOREIGN KEY
(product_id) REFERENCES product (id) ON DELETE NO ACTION ON
UPDATE NO ACTION)
Edit2:
I initially set onupdate="SET NULL" to the order_item entity and thought that was enough, it was not:
/**
* #var \Product
*
* #ORM\ManyToOne(targetEntity="Product")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
* })
*/
private $product;
After that, I had to update db schema as well.

Assuming you have the proper relations set up between the owning entity product and the other entities e.g. cart_item that should have a foreign_key, your wanted behaviour is the default for doctrine 2.
Take a look here in the manual
As an example they show the deletion of a User entity and its corresponding Comments
$user = $em->find('User', $deleteUserId);
foreach ($user->getAuthoredComments() AS $comment) {
$em->remove($comment);
}
$em->remove($user);
$em->flush();
The example states:
Without the loop over all the authored comments Doctrine would use an UPDATE statement only to set the foreign key to NULL and only the User would be deleted from the database during the flush()-Operation.
This suggests to me that in your case you actually want that behaviour. So just remove the product entity and doctrine 2 will automatically find all other entities with a foreign_key belonging to that product and will set it to NULL
Edit
Your error message suggests that upon attempted removal of the product entity there are still foreign_keys present, i.e. they have not been set to null properly by Doctrine.
You need to be sure to add the cascade property, specifically remove to your entity relationship. It would look something like the following:
<?php
class Product
{
//...
/**
* Bidirectional - One-To-Many (INVERSE SIDE)
*
* #OneToMany(targetEntity="Cart", mappedBy="product", cascade={"remove"})
*/
private $carts;
//...
}

Related

Doctrine 2 flushes the wrong entity first

I have this database design and in some instances Doctrine 2 makes the correct decisions in insert order and sometimes not
I have an automated import process that imports and updates data as the data provider changes it. The field item.something_happened_item_history_id is the new field that causes issues
When starting the import
Step 1. No data in the database and I create one item and one item_history, where item_history.item_id is the item.id and item.something_happened_item_history_id is null. Now I do Flush and Doctrine can figure it out that it needs to insert item before item_history. All is good.
Step 2. Now a new import comes in, and some of the data already exists in the database from the Step 1. But in the new import I actually have a new unique item. So what I do is that I create the item and item_history exactly as in Step 1. But for some reason during the Flush Doctrine thinks that item_history needs to be saved before the item. Which can't be done, because of not nullable foreign key reference on item_history.
I understand that Doctrine makes decisions based on foreign key references and the field something_happened_item_history_id is causing trouble. But it doesn't cause any trouble in the step 1. The problems occur when there is already some data in the database that is loaded into the entityManager.
I haven't been able to figure out how to manipulate Doctrine 2 so that it would always save the item before item_history.
Any ideas how to solve it?
As I really do not want to change my database design, because Doctrine 2 can't figure out the insert order.
Also it is not an option to do more Flushes, as there is a lot of data validation done before the data goes to the database. And I do not want any corrupt data in the database.
Best Regards,
Hendrik
EDIT: Doctrine 2 mapping
/**
* #Table(name="item", uniqueConstraints={#UniqueConstraint(name="uc_something_happened_item_history", columns={"something_happened_item_history_id"})})
**/
class Item
{
/** #Id #Column(name="id", type="integer", options={"unsigned":true}) #GeneratedValue **/
protected $id;
/**
* #OneToMany(targetEntity="ItemHistory", mappedBy="item")
**/
protected $itemHistories;
/**
* #OneToOne(targetEntity="ItemHistory")
* #JoinColumn(name="something_happened_item_history_id", referencedColumnName="id", nullable=true, unique=true)
**/
protected $somethingHappenedItemHistory;
}
/**
* #Table(name="item_history")
**/
class ItemHistory
{
/** #Id #Column(name="id", type="integer", options={"unsigned":true}) #GeneratedValue **/
protected $id;
/**
* #ManyToOne(targetEntity="Item", inversedBy="itemHistories")
* #JoinColumn(name="item_id", referencedColumnName="id", nullable=false)
**/
protected $item;
}
I haven't resolved the problem the way I wanted.
But by making the item_id nullable, then the Doctrine is able to finish the flush.
Data is also correct in the database.

Doctrine ManyToMany: removing an object

I have got problem with unidirectional ManyToMany relationship in Doctrine. The case is very easy: Product has many Tags. Tag can be attached to Product but also to any "taggable" entity in my model. Here is snippet of my code:
/**
* #Entity
* #Table(name="products")
**/
class Product {
/** some other fields here */
/**
* #ManyToMany(targetEntity="Tag")
* #JoinTable(name="products_tags",
* joinColumns={#JoinColumn(name="product_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="tag_id", referencedColumnName="id")}
* )
*/
protected $tags;
}
Since its unidirectional relation code of Tag class is omitted.
For such defined association Doctrine generated the following SQL code (SQL for products table and tags table is skipped):
CREATE TABLE `products_tags` (
`product_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`product_id`,`tag_id`),
KEY `IDX_E3AB5A2C4584665A` (`product_id`),
KEY `IDX_E3AB5A2CBAD26311` (`tag_id`),
CONSTRAINT `FK_E3AB5A2CBAD26311` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `FK_E3AB5A2C4584665A` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |
I would like to remove product that has some tags attached to it.
/* $product is already persisted, $em is an Entity Manager */
$em->remove($product);
$em->flush();
It obviously fails due to integrity constraint violation ("Cannot delete or update a parent row: a foreign key constraint fails (products_tags, CONSTRAINT FK_E3AB5A2CBAD26311 FOREIGN KEY (tag_id) REFERENCES tags (id))'").
When I alter products_tags table adding ON DELETE CASCADE to foreign keys it works as I want. I can EASILY remove TAG ($em->remove($tag)) and PRODUCT ($em->remove($product) that automatically removes referenced rows from products_tags table.
How my code should look like to obtain products_tags table with ON CASCADE DELETE foreign keys? I've already tired with cascade={"all"} but it failed.
I know, I can remove all tag from product's tags collection, but as I mentioned I would like to achieve it in one step, just by calling remove method of entity manager object.
Does Doctrine really lack of that?
Ok, I managed myself by digging in Doctrine2 docs ;) Solution is to add onDelete="cascade" to #JoinColumn.
/**
* #Entity
* #Table(name="products")
**/
class Product {
/** some other fileds here */
/**
* #ManyToMany(targetEntity="Tag")
* #JoinTable(name="products_tags",
* joinColumns={#JoinColumn(name="product_id", referencedColumnName="id", onDelete="cascade")},
* inverseJoinColumns={#JoinColumn(name="tag_id", referencedColumnName="id", onDelete="cascade")}
* )
*/
protected $tags;
}
Note that, cascade={"all"} is managed on object level (in your app), while onDelete="cascade" is on database level.

Doctrine2: How to keep custom join table synchronized in a bi-directional Many-to-Many association?

The statement
I'm trying to reproduce the automatic Doctrine mechanism for handling Many-to-Many bidrectional relationships, but introducing a custom join table.
I've already digged into similar questions:
Joining-Table with Metadata Impairs Getters/Setters - Doctrine 2
but it doesn't really help me because it's absolutely unidirectional
doctrine2 many to many self referencing with intermediate details
but this one does not even talk about managing the relations
Doctrine2: Best way to handle many-to-many with extra columns in reference table
is very interesting. However, although the author mentions its bidirectional needs, he doesn't cover the case.
I'm aware that a join table with extra fields is not an association anymore, just a third entity that refers to the two other ones. And from that statement, it's obvious that one cannot expect it to work out-of-the-box as an implicit Many-to-Many association managed by Doctrine.
But i want to have this trio to work as a simple, straight, bidirectional Many-to-Many association, so that means using proxy methods and relying on a Logic class.
The code
There's a Category entity and a Product entity:
/**
* #ORM\Table(name="category")
* #ORM\Entity(repositoryClass="CategoryRepository")
*/
class Category
{
/**
...
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="CategoryProduct", mappedBy="category", fetch="LAZY", cascade={"persist"})
*/
protected $categoryProducts;
}
and
/**
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductRepository")
*/
class Product
{
/**
...
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="CategoryProduct", mappedBy="product", fetch="LAZY", cascade={"persist"})
*/
protected $categoryProducts;
}
and of course a join entity:
/**
* #ORM\Table(name="category_product")
* #ORM\Entity(repositoryClass="CategoryProductRepository")
*/
class CategoryProduct
{
/**
...
*/
protected $id = null;
/**
* #ORM\ManyToOne(targetEntity="Category", fetch="EAGER", inversedBy="categoryProducts")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $category;
/**
* #ORM\ManyToOne(targetEntity="Product", fetch="EAGER", inversedBy="categoryProducts")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $product;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
protected $starred = false;
}
The problem
How to keep an up-to-date list of CategoryProduct entities available to both entities in a pure ORM-style way? In an ORM, everything is managed on the Object layer. Changes to DB are made only on user's request, but it's not compulsory as long as one only works from the ORM point of view. In other words:
$category->addProduct($product);
does not write anything to the DB, and does not even persist any object to the entity manager, but one can still retrieve or remove this product from the list as long as the script runs.
In the case of a custom join table, it's different, because when one wants to add a product, he must create and persist a CategoryProduct entity. So what if we need to retrieve this association from the inverse side?. Here is a code sample that demonstrates my problem:
$product->addCategory($category);
$category->addProduct($product);
In this bidirectional association, how can the $category::addProduct function know about the instance of CategoryProduct entity created by $product::addcategory? The risk is to create two similar join entities for the same association, and i don't know how to avoid it.

Doctrine: complicated select with two ManyToMany relationships

I need to query a number of issues in a table of a issue tracking system limiting this query by a complicated condition:
Issues (Entity) are grouped into categories (another Entity). Persons (Entity) are members of multiple roles (fourth Entity), this is one ManyToMany relationship. And finally, a role can have access on one or many categories, this is the second ManyToMany relationship.
<?php
/**
* #Entity
* #Table(name="issue")
*/
class Issue
{
/**
* #ManyToOne(targetEntity="Category", fetch="EAGER")
* #JoinColumn(name="category", referencedColumnName="id", onDelete="RESTRICT", nullable=false)
*/
private $category;
…
}
/**
* #Entity
* #Table(name="category")
*/
class Category
{
/**
* #ManyToMany(targetEntity="Role", mappedBy="categories")
*/
private $roles;
…
}
/**
* #Entity
* #Table(name="role")
*/
class Role
{
/**
* #ManyToMany(targetEntity="Person", mappedBy="roles")
*/
private $persons;
/**
* #ManyToMany(targetEntity="Category", inversedBy="roles")
* #JoinTable(name="role_has_access_on_category",
* joinColumns={#JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
private $categories;
…
}
/**
* #Entity
* #Table(name="person")
*/
class Person
{
/**
* #ManyToMany(targetEntity="Role", inversedBy="persons")
* #JoinTable(name="person_is_member_of_role",
* joinColumns={#JoinColumn(name="person_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="role_id", referencedColumnName="id")})
*/
private $roles;
…
}
I have left all fields except the relationship ones away, of course there are primary keys and a lot more columns there…
I want to retrieve all issues that belong to categories to which a person with a given primary key has access via the roles it is member of.
At first I just started to experiment how to query ManyToMany relationships, so the code below does not really resemble my target.
I have finally found out how I can get the query to retrieve the other side of one ManyToMany relationship, so I can already get the roles a person belongs to. But this query does not fetch the categories a role has access to.
$qb = $this->em->createQueryBuilder();
$qb->select('person')
->addSelect('role')
->addSelect('category')
->from('Person', 'person')
->innerJoin('person.roles', 'role')
->innerJoin('role.categories', 'category');
$result = $qb->getQuery()->getResult();
$result contains the persons data with all associated roles, but a blank array of categories instead of the entities. The final query would start from the issue side of course, but for now I would only like to get through to the other side…
So now I wonder whether I have to take all the roles and loop through them to fetch all categories. Is there no easier Doctrine way?
By the way, that's the SQL I would use:
SELECT issue.* FROM person AS p, person_is_member_of_role AS pim, role_has_access_on_category AS rha, issue
WHERE
p.id = pim.person_id AND
pim.role_id = rha.role_id AND
rha.category_id = todo.category AND
p.id = ?;
I hope this all makes it clear somehow, otherwise I will revise my question…
You are over-complicating it by trying to put all join conditions in the WHERE clause.
If I understood the question correctly, the query looks like this:
SELECT
i
FROM
Issue i
JOIN
i.category c
JOIN
c.roles r
JOIN
r.persons p
WHERE
p.id = :personId
Translated in QueryBuilder API:
$qb = $entityManager->createQueryBuilder();
$issues = $qb
->select('i')
->from('Issue', 'i')
->innerJoin('i.category', 'c')
->innerJoin('c.roles', 'r')
->innerJoin('r.persons', 'p')
->andWhere($qb->expr()->eq('p.id', ':personId'))
->setParameter('personId', $personId)
->getQuery()
->getResult();
Also, consider avoiding the QueryBuilder if there's no good reason to use it. After all, it's just a string builder.

Symfony2 OneToOne relationship becomes either a Unique index or a foreign key?

I'm taking my first steps in Symfony2 entity relations.
I have an entity installation, which each has one meter, and one monitor.
This translates to a uni-directional relationship, which I defined as such:
/**
*
* #ORM\OneToOne(targetEntity="InfoMeter")
* #ORM\JoinColumn(name="meterid", referencedColumnName="id")
*/
private $meter;
/**
*
* #ORM\OneToOne(targetEntity="InstallationsRtu")
* #ORM\JoinColumn(name="monitorid", referencedColumnName="id")
*/
private $monitor;
Each monitor can only be assigned to one installation.
Each meter can be assigned to multiple installations.
When I update my database (app/console doctrine:schema:update --force), there are two outcomes.
In case of the monitor:
Everything goes alright, schema gets updated with a keyname which is prefixed by 'UNIQ_'.
In case of the meter:
I get the following error
PDOException]
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1' for key 'UNIQ_43D26968860BE41D'
When I look at the table structure after this error occured, I can't find that mentioned constraint, but I do find FK_43D26968860BE41D, i.e. the same, but prefixed with 'FK'.
In the installation table, I now have these listed:
Keyname Type Unique Packed Field Cardinality
PRIMARY BTREE Yes No id 2
UNIQ_43D26968701EC1AB BTREE Yes No monitorid 2
FK_43D26968860BE41D BTREE No No meterid 2
So one saying 'Unique=Yes', and the other saying 'Unique=No'.
To arrive at my question:
How can I decide whether it is a UNIQ index or a FK index?
I assume that Doctrine saw that currently each monitorid is unique in the installation table, but that each meterid occurs several times in the installation table.
Hence it went with UNIQ for the first, and FK for the latter. But can I control this somehow?
If one metter can be assigned to multiple installations, shouldn't you define a OneToMany relationship?
In your installation entity:
/**
* #ORM\ManyToOne(targetEntity="InfoMeter", inversedBy="installations")
* #ORM\JoinColumn(name="infometer_id", referencedColumnName="id")
*/
protected $info_meter;
And then in your InfoMeter entity:
/**
* #ORM\OneToMany(targetEntity="Installation",mappedBy="info_meter")
*/
protected $installations;
Besides, you should add the following to your InfoMeter class constructor:
function __construct() {
[...]
$this->installations = new \Doctrine\Common\Collections\ArrayCollection();
}
I'm sure this approach can be improved depending on how you want the relation between "installations" and "meters", but this should work.