11

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?

Petah
  • 45,477
  • 28
  • 157
  • 213
  • Sorry for the erroneous close vote, I misread your question. What you need to do is remove the actual foreign key constraint. If the constraint was created via Doctrine, you'll want to configure it to not do so again or you'll just have to keep removing it – Phil Sep 06 '16 at 23:36
  • @Phil Doctine is generating the schema though, and I don't know how to tell it not to add the constraint. – Petah Sep 06 '16 at 23:46
  • 1
    An alternative may be to add a property (e.g., `enabled`) that would exclude the page from searches. Doing so creates the effect of the page being deleted without violating a foreign key constraint. – geoB Sep 06 '16 at 23:51
  • I looked around and couldn't find anything substantial either. You could try running a post-creation script to remove the constraint. Or perhaps you could change to MyISAM tables which don't support foreign keys. – Phil Sep 06 '16 at 23:51
  • 2
    @Phil I think that even with that, there may be further issues with data inconsistency from Doctrine's point of view. I would suggest to implement soft-delete of page (my favorite in this case) or duplicate `page_id` column as "backup" and then set `null` on `page_id` when removing page. – Jakub Matczak Sep 07 '16 at 06:20
  • @dragoste it seems OP actually wants data inconsistency. I think geoB's [soft-delete comment above](http://stackoverflow.com/questions/39358986/disable-doctrine-foreign-key-constraint?noredirect=1#comment66048431_39358986) is the best solution too – Phil Sep 07 '16 at 06:22
  • 2
    Soft delete is probably the most correct solution, but it would be time consuming to alter all other parts of the app to filter out deleted entities. Switch to MyISAM also works, which is what I have done, but is not ideal. – Petah Sep 07 '16 at 08:16
  • 2
    @Petah If you implements soft-delete with [gedmo](https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/softdeleteable.md) you don't need to alter anything to filter out deleted entities, just add the filter as default in your config file `doctrine: orm: entity_managers: default: filters: softdeleteable: [class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter & enabled: true]`. Then, whenever you want to fetch deleted entites just temporarly disable the filter `$filters = ...->get('doctrine')->getEntityManager()->getFilters(); $filters->disable('softdeleteable'); query then enable` – lolmx Sep 09 '16 at 12:39

5 Answers5

17

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.

Wilt
  • 41,477
  • 12
  • 152
  • 203
  • Guaranteed constraints require additional computing resources. The application server is easier to scale horizontally than the database server, so the constraints should be placed in the business logic. – Moon soon Aug 12 '19 at 04:51
3

I solved this by overriding one doctrine class in symfony 4.3, it looks like this for me:

enter image description here

enter image description here

enter image description here

<?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;
    }
}
2

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.

Ross Keddy
  • 141
  • 2
  • 8
2

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').

Stephan Vierkant
  • 9,674
  • 8
  • 61
  • 97
1

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.

cezar
  • 11,616
  • 6
  • 48
  • 84
Théo
  • 655
  • 7
  • 17
  • 2
    MySQL is not a database engine, it is a DBMS. Popular database engines for MySQL (InnoDB and MyISAM) handle differently with foreign keys. If you want to know more about it, read this question: http://stackoverflow.com/questions/12614541/whats-the-difference-between-myisam-and-innodb – Stephan Vierkant Sep 13 '16 at 17:42
  • 8
    `$objectManager->getConnection()->exec('SET FOREIGN_KEY_CHECKS = 0;');` – Théo Jan 23 '17 at 12:42