En eliminar cascada con doctrine2

227

Estoy tratando de hacer un ejemplo simple para aprender cómo eliminar una fila de una tabla primaria y eliminar automáticamente las filas coincidentes en la tabla secundaria usando Doctrine2.

Aquí están las dos entidades que estoy usando:

Child.php:

<?php

namespace Acme\CascadeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="child")
 */
class Child {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @ORM\ManyToOne(targetEntity="Father", cascade={"remove"})
     *
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="father_id", referencedColumnName="id")
     * })
     *
     * @var father
     */
    private $father;
}

Father.php

<?php
namespace Acme\CascadeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="father")
 */
class Father
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
}

Las tablas se crean correctamente en la base de datos, pero no se crea la opción On Delete Cascade. ¿Qué estoy haciendo mal?

rfc1484
fuente
¿Has probado si las cascadas funcionan correctamente de todos modos? Quizás Doctrine los maneja en código en lugar de en la base de datos.
Problemática el

Respuestas:

408

Hay dos tipos de cascadas en Doctrine:

1) Nivel ORM - usos cascade={"remove"}en la asociación - este es un cálculo que se realiza en UnitOfWork y no afecta la estructura de la base de datos. Cuando elimina un objeto, UnitOfWork iterará sobre todos los objetos de la asociación y los eliminará.

2) Nivel de base de datos - usos onDelete="CASCADE"en joinColumn de la asociación - esto agregará On Delete Cascade a la columna de clave externa en la base de datos:

@ORM\JoinColumn(name="father_id", referencedColumnName="id", onDelete="CASCADE")

También quiero señalar que la forma en que tiene su cascada = {"eliminar"} en este momento, si elimina un objeto secundario, esta cascada eliminará el objeto principal. Claramente no es lo que quieres.

Michael Ridgway
fuente
3
Generalmente uso onDelete = "CASCADE" porque significa que el ORM tiene que hacer menos trabajo y debería tener un rendimiento un poco mejor.
Michael Ridgway
58
Yo también, pero depende. Digamos, por ejemplo, que tiene una galería de imágenes con imágenes. Cuando elimine la galería, también desea que las imágenes se eliminen del disco. Si implementa eso en el método delete () de su objeto de imagen, la eliminación en cascada usando el ORM se asegurará de que se invoquen todas las funciones delte () de su imagen, lo que le ahorrará el trabajo de implementar cronjobs que verifican los archivos de imagen huérfanos.
gripe
44
@Michael Ridgway a veces se deben aplicar ambas declaraciones, onDeleteasí como, cascade = {"remove"}por ejemplo, cuando tiene algún objeto relacionado con fosUser. Ambos objetos no deberían existir solos
Luke Adamczewski
17
Tenga en cuenta que puede escribir @ORM\JoinColumn(onDelete="CASCADE")y dejar que la doctrina maneje los nombres de las columnas automáticamente.
mcfedr
55
@dVaffection Esa es una buena pregunta. Creo que onDelete="CASCADE"no tendrá ningún efecto ya que Doctrine cascade={"remove"}elimina las entidades relacionadas antes de eliminar la entidad raíz (tiene que hacerlo). Entonces, cuando se elimina la entidad raíz, no quedan relaciones exteriores para onDelete="CASCADE"eliminar. Pero para estar seguro, sugeriría que simplemente cree un pequeño caso de prueba y observe las consultas que se ejecutan y su orden de ejecución.
gripe
50

Aquí hay un ejemplo simple. Un contacto tiene uno o muchos números de teléfono asociados. Cuando se elimina un contacto, quiero que también se eliminen todos sus números de teléfono asociados, así que uso ON DELETE CASCADE. La relación uno a muchos / muchos a uno se implementa con la clave externa en los números de teléfono.

CREATE TABLE contacts
 (contact_id BIGINT AUTO_INCREMENT NOT NULL,
 name VARCHAR(75) NOT NULL,
 PRIMARY KEY(contact_id)) ENGINE = InnoDB;

CREATE TABLE phone_numbers
 (phone_id BIGINT AUTO_INCREMENT NOT NULL,
  phone_number CHAR(10) NOT NULL,
 contact_id BIGINT NOT NULL,
 PRIMARY KEY(phone_id),
 UNIQUE(phone_number)) ENGINE = InnoDB;

ALTER TABLE phone_numbers ADD FOREIGN KEY (contact_id) REFERENCES \
contacts(contact_id) ) ON DELETE CASCADE;

Al agregar "ON DELETE CASCADE" a la restricción de clave externa, phone_numbers se eliminará automáticamente cuando se elimine su contacto asociado.

INSERT INTO table contacts(name) VALUES('Robert Smith');
INSERT INTO table phone_numbers(phone_number, contact_id) VALUES('8963333333', 1);
INSERT INTO table phone_numbers(phone_number, contact_id) VALUES('8964444444', 1);

Ahora, cuando se elimina una fila en la tabla de contactos, todas sus filas asociadas phone_numbers se eliminarán automáticamente.

DELETE TABLE contacts as c WHERE c.id=1; /* delete cascades to phone_numbers */

Para lograr lo mismo en Doctrine, para obtener el mismo comportamiento de nivel de base de datos "ON DELETE CASCADE", debe configurar @JoinColumn con la opción onDelete = "CASCADE" .

<?php
namespace Entities;

use Doctrine\Common\Collections\ArrayCollection;

/**
 * @Entity
 * @Table(name="contacts")
 */
class Contact 
{

    /**
     *  @Id
     *  @Column(type="integer", name="contact_id") 
     *  @GeneratedValue
     */
    protected $id;  

    /** 
     * @Column(type="string", length="75", unique="true") 
     */ 
    protected $name; 

    /** 
     * @OneToMany(targetEntity="Phonenumber", mappedBy="contact")
     */ 
    protected $phonenumbers; 

    public function __construct($name=null)
    {
        $this->phonenumbers = new ArrayCollection();

        if (!is_null($name)) {

            $this->name = $name;
        }
    }

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function addPhonenumber(Phonenumber $p)
    {
        if (!$this->phonenumbers->contains($p)) {

            $this->phonenumbers[] = $p;
            $p->setContact($this);
        }
    }

    public function removePhonenumber(Phonenumber $p)
    {
        $this->phonenumbers->remove($p);
    }
}

<?php
namespace Entities;

/**
 * @Entity
 * @Table(name="phonenumbers")
 */
class Phonenumber 
{

    /**
    * @Id
    * @Column(type="integer", name="phone_id") 
    * @GeneratedValue
    */
    protected $id; 

    /**
     * @Column(type="string", length="10", unique="true") 
     */  
    protected $number;

    /** 
     * @ManyToOne(targetEntity="Contact", inversedBy="phonenumbers")
     * @JoinColumn(name="contact_id", referencedColumnName="contact_id", onDelete="CASCADE")
     */ 
    protected $contact; 

    public function __construct($number=null)
    {
        if (!is_null($number)) {

            $this->number = $number;
        }
    }

    public function setPhonenumber($number)
    {
        $this->number = $number;
    }

    public function setContact(Contact $c)
    {
        $this->contact = $c;
    }
} 
?>

<?php

$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);

$contact = new Contact("John Doe"); 

$phone1 = new Phonenumber("8173333333");
$phone2 = new Phonenumber("8174444444");
$em->persist($phone1);
$em->persist($phone2);
$contact->addPhonenumber($phone1); 
$contact->addPhonenumber($phone2); 

$em->persist($contact);
try {

    $em->flush();
} catch(Exception $e) {

    $m = $e->getMessage();
    echo $m . "<br />\n";
}

Si ahora lo haces

# doctrine orm:schema-tool:create --dump-sql

verá que se generará el mismo SQL que en el primer ejemplo de SQL sin formato

Kurt Krueckeberg
fuente
44
¿Es la colocación correcta? Eliminar el número de teléfono no debería eliminar el contacto. Es el contacto a quien la eliminación debería desencadenar en cascada. ¿Por qué entonces colocar la cascada en el niño / teléfono?
przemo_li
1
@przemo_li Es la colocación correcta. El contacto no sabe que existen números de teléfono, porque los números de teléfono tienen una referencia al contacto y un contacto no tiene una referencia a los números de teléfono. Entonces, si un contacto se elimina, un número de teléfono tiene una referencia a un contacto no existente. En este caso, queremos que suceda algo: desencadenar la acción ON DELETE. Decidimos poner en cascada la eliminación, para eliminar también los números de teléfono.
marijnz0r
3
@przemi_li onDelete="cascade"se coloca correctamente en la entidad (en el elemento secundario) porque se trata de una conexión en cascada de SQL , que se coloca en el elemento secundario. Solo el Doctrine en cascada ( cascade=["remove"]que no se usa aquí) se coloca en el padre.
Maurice