Disable Doctrine foreign key constraint - php

I have a relationship on one of my models:
/**
* #ORM\ManyToOne(targetEntity="Page", cascade="persist")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*/
private $parentPage;
And when I delete the parent page, I get this error:
Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails
Basically my models are a page, and page revision. When I delete the page I don't want to delete the revisions. I also want to keep the page_id on the page revisions (i.e. not set it to null).
How can I do this with Doctrine?

By definition you cannot delete the record that the foreign key is pointing at without setting the key to null (onDelete="SET NULL") or cascading the delete operation (There are two options - ORM Level: cascade={"remove"} | database level: onDelete="CASCADE"). There is the alternative of setting a default value of a still existing record, but you have to do that manually, I don't think Doctrine supports this "out-of-the-box" (please correct me if I am wrong, but in this case setting a default value is not desired anyway).
This strictness is reflecting the concept of having foreign key constraints; like #Théo said:
a FK is to ensure data consistency.
Soft delete (already mentioned) is one solution, but what you could also do is add an additional removed_page_id column that you sync with the page_id just before you delete it in a preRemove event handler (life cycle callback). Whether such information has any value I wonder but I guess you have some use for it, otherwise you wouldn't ask this question.
I am definitely not claiming this is good practice, but it is at least something that you can use for your edge case. So something in the line of:
In your Revision:
/**
* #ORM\ManyToOne(targetEntity="Page", cascade="persist")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parentPage;
/**
* #var int
* #ORM\Column(type="integer", name="removed_page_id", nullable=true)
*/
protected $removedPageId;
And then in your Page:
/**
* #ORM\PreRemove
*/
public function preRemovePageHandler(LifecycleEventArgs $args)
{
$entityManager = $args->getEntityManager();
$page = $args->getEntity();
$revisions = $page->getRevisions();
foreach($revisions as $revision){
$revision->setRemovedPageId($page->getId());
$entityManager->persist($revision);
}
$entityManager->flush();
}
Alternatively you could of course already set the correct $removedPageId value during construction of your Revision, then you don't even need to execute a life cycle callback on remove.

I solved this by overriding one doctrine class in symfony 4.3, it looks like this for me:
<?php declare(strict_types=1);
namespace App\DBAL;
use Doctrine\DBAL\Platforms\MySQLPlatform;
/**
* Class MySQLPlatformService
* #package App\DBAL
*/
class MySQLPlatformService extends MySQLPlatform
{
/**
* Disabling the creation of foreign keys in the database (partitioning is used)
* #return false
*/
public function supportsForeignKeyConstraints(): bool
{
return false;
}
/**
* Disabling the creation of foreign keys in the database (partitioning is used)
* #return false
*/
public function supportsForeignKeyOnUpdate(): bool
{
return false;
}
}

You can disable the exporting of foreign keys for specific models:
User:
attributes:
export: tables
columns:
Now it will only export the table definition and none of the foreign keys. You can use: none, tables, constraints, plugins, or all.

You are explicitly asking for data inconsistency, but I'm pretty sure you really don't want that. I can't think of a situation where this would be defensible. It is a bad practice and definitely will cause problems. For example: what is the expected result of $revision->getPage()?
There is a very simple and elegant solution: softdeletable. It basically adds an attribute to your entity (in other words: adds column to your table) named deletedAt to store if (or better: when) that entity is deleted. So if that attribute is null, the entity isn't deleted.
The only thing you have to do is add this bundle, add a trait to your entity (Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity) and update your database. It is very simple to implement: this package will do the work for you. Read the documentation to understand this extension.
Alternatively, you can add an 'enabled' boolean attribute or a status field (for example 'published', 'draft', 'deleted').

When I delete the page I don't want to delete the revisions. I also want to keep the page_id on the page revisions (i.e. not set it to null).
I think you already got your answer: Doctrine won't do that, simply because it's alien to the notion of Foreign Keys. The principle of a FK is to ensure data consistency, so if you have a FK, it must refer to an existing ID. On delete, some DB engine such as InnoDB for MySQL allow you to put an FK to NULL (assuming you did made the FK column nullable). But referring to an inexistent ID is not doable, or it's not a FK.
If you really want to do it, don't use Doctrine for this specific case, it doesn't prevent you to use Doctrine elsewhere in your codebase. Another solution is to just drop the FK constraint manually behind or use a DB statement before your query to skip the FK checks.

Related

doctrine migration: default value for not nullable fk

I have an existing entity in database. I would like to add a new column to this entity:
/**
* #ORM\ManyToOne(targetEntity="Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id", nullable=false)
*/
protected $language;
When I now use "vendor/bin/doctrine-migrate migrations:diff", the generated migration script does not contain a default value for language_id. Therefore the migration-script fails. Setting a default value for the property in the object does not help. How can I define a default value for the fk-column? I neither found something in doctrines documentation nor through google/stackoverflow.
If the column is not null then it should represent a valid relationship; assuming you use 0 instead, Doctrine will try to load the association using that, which would of course fail. In these cases you would need to update the database and mapping to allow a null value.
If however you require a default language association to be defined then you explicitly need to set it when you create the entity.
$language = $entityManager->find(1);
$entity = new Entity;
$entity->setLanguage($language);
$entityManager->persist($entity);
$entityManager->flush();
For this reason, you might want to consider a 'service' that encapsulates the creation of your entity so you know a language will always be valid and assigned by default.
It may not be a clean way to do it, but maybe you can execute the SQL query yourself and add manually the DEFAULT statement ?
ALTER TABLE registered_user ADD language_id VARCHAR(255) NOT NULL DEFAULT {default_value} WITH VALUES;
I'm quite surprised that adding a default property in the annotation is not working here!

Doctrine's "options" property in #UniqueConstraint not working

Context
I'm using Doctrine version 2.5.0 and I have two entities: Group and Item. I'm trying to create an unique constraint so that Items cannot have the same position in a Group:
/**
* #Entity
* #Table(uniqueConstraints={
* #UniqueConstraint(name="position", columns={"group_id", "position"})
* )
*/
class Item {
...
}
This works well.
Problem
I added an active field to the Item entity, so instead of deleting an Item, I 'inactivate' it. But now the unique constraint doesn't work anymore, since the Item stays in the database with his Group reference and his position.
Attempts
Looking at the Doctrine docs, I've discovered that I can use the options property with a where clause in the #UniqueConstraint:
/**
* #Entity
* #Table(uniqueConstraints={
* #UniqueConstraint(name="position", columns={"group_id", "position"},
* options={"where":"(active = 1)"})}
* )
*/
class Item {
...
}
But the I get the same error as before:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'1-1' for key 'position'
Here is my deletion code:
$item->setActive(false);
$this->_em->persist($item);
$this->_em->flush();
foreach ($item->getNextItems() as $nextItem) {
$nextItem->setPosition($nextItem->getPosition() - 1);
$this->_em->persist($nextItem);
}
$this->_em->flush();
Any idea why the options property is not working?
Update
I realised a strange behaviour. Every time I run the command ./doctrine orm:schema-tool:update --force it recreates the index:
DROP INDEX position ON Item;
CREATE UNIQUE INDEX position ON Item (group_id, position);
But once I remove the options property and run the command, I get:
Nothing to update - your database is already in sync with the current
entity metadata.
It seems I read over this from the Doctrine docs:
SQL WHERE condition to be used for partial indexes. It will only have effect on supported platforms.
After research:
MySQL does not support partial indexes of this nature
My solution is to check if the Item is really unique in a LifeCycleEvent inside the entity:
/**
* #PostPersist #PostUpdate
*/
public function checkUnicity(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
$em = $eventArgs->getEntityManager();
// checking unicity here
// throw exception if not
}
if you have data already in database and then after you are adding unique constraint first please check in database that you have unique values or not.

Cannot delete a records (OneToMany) with Symfony

I have 2 entities linked together PICTURE <-(OneToMany)-> NOTE (see class details below). When I tried to delete a picture record I got the following message from Symfony
An exception occurred while executing 'DELETE FROM Picture WHERE id =
?' with params [118]: SQLSTATE[23000]: Integrity constraint violation:
1451 Cannot delete or update a parent row: a foreign key constraint
fails (symfony.note, CONSTRAINT FK_6F8F552A8671F084 FOREIGN KEY
(picture_id) REFERENCES Picture (id))
I find it strange because if the note.author_id is the same as the current user, then I can delete the picture (and the note associated to the picture) without any problem.
here some details of my class picture and note:
Class Picture
{
/**
* #ORM\OneToMany(targetEntity="XXX\XXXBundle\Entity\Note", mappedBy="picture", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="note_id", referencedColumnName="picture_id", onDelete="CASCADE")
**/
private $notes;
}
Class Note
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="XXX\XXXBundle\Entity\Picture", inversedBy="notes")
**/
private $picture;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Sdz\UserBundle\Entity\User", cascade={"persist"})
*/
private $user;
}
Here my controller to delete the picture. FYI picture is a collection in my form. I set the option allow_delete to true
foreach($pictures as $picture) // $pictures is the initial list of picture
{
if(false === $data_form->getPictures()->contains($picture))
{
$em->remove($picture);
}
}
You have constraints on database that prevents you from cascaded deleting. I would suggest to use phpMyAdmin tool (mysql command line would be horrible to use for checking constraints): open both tables structure tab, go into relational view, and here check how constraints are set.
You may have different constraints set inside doctrine mappings and inside database, and it may work fine, until you try to perform an action on database, that collides with database constraints, like here.

Integrity constraint violation: 1062 Duplicate entry '4974-134' for key 'UNQ_CATALOG_PRODUCT_SUPER_ATTRIBUTE_PRODUCT_ID_ATTRIBUTE_ID'

I am getting the above error in Magento when adding a configurable product (before creating simple products)
This has worked but for some reason it is now failing.
The key value 4974-134 doesn't even exist in the table:
I've tried re-creating the table. I''ve cleare cache/log tables/re-indexed and nothing seems to work - each time the 4974 (product/entity_id) increments by 1 implying it is being created in the catalog_product_entity table but it isn't:
The only way that I could resolve this eventually was to extend/overwrite the Product model _afterSave function in a new module (make sure the new class extends extends Mage_Catalog_Model_Product).
Like so:
/**
* Saving product type related data and init index
*
* #return Mage_Catalog_Model_Product
*/
protected function _afterSave()
{
$this->getLinkInstance()->saveProductRelations($this);
if($this->getTypeId() !== 'configurable')
{
$this->getTypeInstance(true)->save($this);
}
/**
* Product Options
*/
$this->getOptionInstance()->setProduct($this)
->saveOptions();
$result = parent::_afterSave();
Mage::getSingleton('index/indexer')->processEntityAction(
$this, self::ENTITY, Mage_Index_Model_Event::TYPE_SAVE
);
return $result;
}
The key bit being:
if($this->getTypeId() !== 'configurable')
{
$this->getTypeInstance(true)->save($this);
}
}
It looks like for some reason, when creating the configurable product it was trying to save an object that already existed in the resource adapter possibly - Some thoughts on this would be appreciated.
It's a combination of the product_id and the attribute_id. Whats interesting is 4795 already exists in there.
I had this issue once after Host Gator transferred a Magento site for me.
Find out which tables are being used as the FK for these two fiends and make sure that the next increment ID isn't lower than the highest in this table.
In my case, one of the tables auto_increment ID was reset to 0 so it was trying to create a "1", but that foreign key was already used in the offending table.
Hope this helps.

Doctrine2.0: Error - Column specified twice

In Doctrine2.0.6, I keep getting an error: "Column VoucherId specified twice".
The models in question are:
Basket
BasketVoucher
Voucher
Basket links to BasketVoucher.
Voucher links to BasketVoucher.
In Voucher and BasketVoucher, there is a field called VoucherId. This is defined in both models and exists with the same name in both DB tables.
The error occurs when saving a new BasketVoucher record:
$basketVoucher = new BasketVoucher;
$basketVoucher->setVoucherId($voucherId);
$basketVoucher->setBasketId($this->getBasket()->getBasketId());
$basketVoucher->setCreatedDate(new DateTime("now"));
$em->persist($basketVoucher);
$em->flush();
I've checked the models and VoucherId is not defined twice. However, it is used in a mapping. Is this why Doctrine thinks that the field is duplicated?
Here's the relevant code - I haven't pasted the models in their entirety as most of the code is get/set.
Basket
/**
* #OneToMany(targetEntity="BasketVoucher", mappedBy="basket")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basketVouchers;
public function getVouchers()
{
return $this->basketVouchers;
}
BasketVoucher
/**
* #ManyToOne(targetEntity="Basket", inversedBy="basketVouchers")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basket;
public function getBasket()
{
return $this->basket;
}
/**
* #OneToOne(targetEntity="Voucher", mappedBy="basketVoucher")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $voucherEntity;
public function getVoucher()
{
return $this->voucherEntity;
}
Voucher
/**
* #OneToOne(targetEntity="BasketVoucher", inversedBy="voucherEntity")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $basketVoucher;
public function getBasketVoucher()
{
return $this->basketVoucher;
}
Any ideas?
EDIT: I've found that the same issue occurs with another model when I save it for the first time. I am setting the primary key manually. The main issue appears to be saving a relationship within an entity.
In this case, I have a field - DraftOrderId - which is used as the primary key on three models. The first model - DraftOrder - has DraftOrderId as a primary key, which is an auto incrementing value. The other two models - DraftOrderDeliveryAddress, and DraftOrderBillingAddress - also use DraftOrderId as a primary key, but it isn't auto incremented.
What's happening is one of the following issues:
If I save the delivery address entity with a draft order id and set it to persist, I get an error: Column DraftOrderId specified twice. Code:
try {
$addressEntity->getDraftOrderId();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
if ($addressType == "delivery") {
$addressEntity = new Dpp\DraftOrderDeliveryAddress;
} elseif ($addressType == "billing") {
$addressEntity = new Dpp\DraftOrderBillingAddress;
}
$addressEntity->setDraftOrderId($draftOrder->getDraftOrderId());
$em->persist($addressEntity);
}
(It would also help to know if there's a better way of checking if a related entity exists, rather than trapping the exception when trying to get a value.)
If I remove the line that sets the draft order id, I get an error: Entity of type Dpp\DraftOrderDeliveryAddress is missing an assigned ID.
If I keep the line that sets the draft order id but I remove the persist line, and I also keep the lines later on in the code that sets the name and address fields, I don't get an error - but the data is not saved to the database. I am using flush() after setting all the fields - I'm just not using persist(). In the previous examples, I do use persist() - I'm just trying things out to see how this can work.
I can paste more code if it would help.
I think I've fixed it! A couple of findings:
For a primary key that is not an auto-incrementing value, you need to use:
#generatedValue(strategy="IDENTITY")
You also have to explicitly set the mapped entities when creating them for the first time. At first, I was trying to create the address entity directly, but I wasn't setting the mapped entity within the parent model to reference the address entity. (if that makes any sense)
I'm fairly sure it was mostly due to the lack of the IDENTITY keyword, which for some reason was either saying the key wasn't set, or saying it was set twice.

Categories