Eliminar automáticamente filas relacionadas en Laravel (ORM elocuente)

158

Cuando borro una fila usando esta sintaxis:

$user->delete();

¿Hay alguna forma de adjuntar una especie de devolución de llamada, de modo que, por ejemplo, haga esto automáticamente:

$this->photo()->delete();

Preferiblemente dentro de la clase de modelo.

Martti Laine
fuente

Respuestas:

205

Creo que este es un caso de uso perfecto para eventos Eloquent ( http://laravel.com/docs/eloquent#model-events ). Puede usar el evento "eliminar" para hacer la limpieza:

class User extends Eloquent
{
    public function photos()
    {
        return $this->has_many('Photo');
    }

    // this is a recommended way to declare event handlers
    public static function boot() {
        parent::boot();

        static::deleting(function($user) { // before delete() method call this
             $user->photos()->delete();
             // do the rest of the cleanup...
        });
    }
}

Probablemente también debería poner todo dentro de una transacción, para garantizar la integridad referencial.

Ivanhoe
fuente
77
Nota: paso algún tiempo hasta que esto funcione. Necesitaba agregar first()a la consulta para poder acceder al evento modelo, por ejemplo, User::where('id', '=', $id)->first()->delete(); Fuente
Michel Ayres
66
@MichelAyres: sí, debe llamar a delete () en una instancia de modelo, no en Query Builder. Constructor tiene su propio método de eliminación () que, básicamente, sólo se ejecuta una consulta SQL DELETE, así que supongo que no sabe nada acerca de los eventos ORM ...
Ivanhoe
3
Este es el camino a seguir para las eliminaciones suaves. Creo que la forma nueva / preferida de Laravel es pegar todo esto en el método boot () de AppServiceProvider de esta manera: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
Watercayman
44
Casi trabajé en Laravel 5.5, tuve que agregar un foreach($user->photos as $photo), luego $photo->delete()para asegurarme de que a cada niño se le quitaran los niños en todos los niveles, en lugar de solo uno, ya que estaba sucediendo por alguna razón.
George
9
Sin embargo, esto no lo conecta en cascada. Por ejemplo, si Photostiene tagsy hace lo mismo en el Photosmodelo (es decir, en el deletingmétodo $photo->tags()->delete();:) nunca se activa. Pero si hago un forbucle y hago algo así for($user->photos as $photo) { $photo->delete(); }, ¡ tagstambién se borrará! solo para tu información
supersan
200

De hecho, puede configurar esto en sus migraciones:

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

Fuente: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

También puede especificar la acción deseada para las propiedades "al eliminar" y "al actualizar" de la restricción:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
fuente
Sí, supongo que debería haber aclarado esa dependencia.
Chris Schmitz
62
Pero no si está utilizando eliminaciones suaves, ya que las filas no se eliminan realmente.
temblor
77
También - esto eliminará el registro en la base de datos, pero no se ejecutará el método de eliminación, por lo que si usted está haciendo un trabajo extra en la eliminación (por ejemplo - borrar archivos), no se ejecutará
amosmos
10
Este enfoque se basa en DB para hacer la eliminación en cascada, pero no todos los DB lo admiten, por lo que se requiere un cuidado adicional. Por ejemplo, MySQL con el motor MyISAM no funciona, ni ninguna base de datos NoSQL, SQLite en la configuración predeterminada, etc. El problema adicional es que Artisan no le advertirá sobre esto cuando ejecute migraciones, simplemente no creará claves foráneas en las tablas MyISAM y cuando luego elimine un registro, no ocurrirá ninguna cascada. Tuve este problema una vez y créanme que es muy difícil de depurar.
ivanhoe
1
@kehinde El enfoque que usted muestra NO invoca eventos de eliminación en las relaciones que se van a eliminar. Debe iterar sobre la relación y llamar a eliminar individualmente.
Tom
51

Nota : Esta respuesta fue escrita para Laravel 3 . Por lo tanto, podría o no funcionar bien en la versión más reciente de Laravel.

Puede eliminar todas las fotos relacionadas antes de eliminar realmente al usuario.

<?php

class User extends Eloquent
{

    public function photos()
    {
        return $this->has_many('Photo');
    }

    public function delete()
    {
        // delete all related photos 
        $this->photos()->delete();
        // as suggested by Dirk in comment,
        // it's an uglier alternative, but faster
        // Photo::where("user_id", $this->id)->delete()

        // delete the user
        return parent::delete();
    }
}

Espero eso ayude.

akhy
fuente
1
Tienes que usar: foreach ($ this-> fotos como $ photo) ($ this-> fotos en lugar de $ this-> fotos ()) De lo contrario, ¡buen consejo!
Barryvdh
20
Para hacerlo más eficiente, use una consulta: Photo :: where ("user_id", $ this-> id) -> delete (); No es la mejor manera, pero solo 1 consulta, mucho mejor rendimiento si un usuario tiene 1,000,000 de fotos.
Dirk
55
en realidad puedes llamar: $ this-> photos () -> delete (); no hay necesidad de bucle - ivanhoe
ivanhoe
44
@ivanhoe Noté que el evento de eliminación no se disparará en la foto si elimina la colección, sin embargo, iterar como sugiere akhyar hará que el evento de eliminación se active. ¿Es esto un error?
adamkrell
1
@akhyar Casi, puedes hacerlo con $this->photos()->delete(). El photos()devuelve el objeto generador de consultas.
Sven van Zoelen
32

Relación en modelo de usuario:

public function photos()
{
    return $this->hasMany('Photo');
}

Eliminar registro y relacionado:

$user = User::find($id);

// delete related   
$user->photos()->delete();

$user->delete();
Calin Blaga
fuente
44
Esto funciona, pero tenga cuidado de usar $ user () -> relacion () -> detach () si hay una tabla dinámica involucrada (en el caso de las relaciones hasMany / belongToMany), o de lo contrario eliminará la referencia, no la relación .
James Bailey, el
Esto funciona para mí laravel 6. @Calin ¿puedes explicar más pls?
Arman H
20

Hay 3 enfoques para resolver esto:

1. Uso de eventos elocuentes en el arranque del modelo (ref: https://laravel.com/docs/5.7/eloquent#events )

class User extends Eloquent
{
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->delete();
        });
    }
}

2. Uso de observadores de eventos elocuentes (ref: https://laravel.com/docs/5.7/eloquent#observers )

En su AppServiceProvider, registre el observador así:

public function boot()
{
    User::observe(UserObserver::class);
}

A continuación, agregue una clase Observer así:

class UserObserver
{
    public function deleting(User $user)
    {
         $user->photos()->delete();
    }
}

3. Uso de restricciones de clave externa (ref: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
fuente
1
Creo que las 3 opciones son las más elegantes, ya que está construyendo la restricción en la base de datos. Lo pruebo y funciona bien.
Gilbert
14

A partir de Laravel 5.2, la documentación establece que este tipo de controladores de eventos deben registrarse en AppServiceProvider:

<?php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::deleting(function ($user) {
            $user->photos()->delete();
        });
    }

Incluso supongo moverlos a clases separadas en lugar de cierres para una mejor estructura de aplicación.

Attila Fulop
fuente
1
Laravel 5.3 recomienda ponerlos en clases separadas llamadas Observadores , aunque solo está documentado en 5.3, el Eloquent::observe()método también está disponible en 5.2 y se puede usar desde AppServiceProvider.
Leith
3
Si tiene alguna relación 'hasMany' de su parte photos(), también deberá tener cuidado: este proceso no eliminará a los nietos porque no está cargando modelos. Tendrá que recorrer photos(tenga en cuenta que no photos()) y activar el delete()método en ellos como modelos para activar los eventos relacionados con la eliminación.
Leith
1
@Leith El método de observación también está disponible en 5.1.
Tyler Reed
2

Es mejor si anula el deletemétodo para esto. De esa manera, puede incorporar transacciones de DB dentro del deletemétodo mismo. Si utiliza la forma de evento, deberá cubrir su llamada de deletemétodo con una transacción de base de datos cada vez que la llame.

En tu Usermodelo.

public function delete()
{
    \DB::beginTransaction();

     $this
        ->photo()
        ->delete()
    ;

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
fuente
1

En mi caso fue bastante simple porque las tablas de mi base de datos son InnoDB con claves foráneas con Cascade en Delete.

Entonces, en este caso, si su tabla de fotos contiene una referencia de clave externa para el usuario, lo único que tiene que hacer es eliminar el hotel y la base de datos realizará la limpieza, la base de datos eliminará todos los registros de fotos de los datos base.

Alex
fuente
Como se ha observado en otras Respuestas, las eliminaciones en cascada en la capa de la base de datos no funcionarán cuando se utilicen las eliminaciones suaves. El comprador tenga cuidado. :)
Ben Johnson
1

Recorrería la colección separando todo antes de eliminar el objeto en sí.

Aquí hay un ejemplo:

try {
        $user = user::findOrFail($id);
        if ($user->has('photos')) {
            foreach ($user->photos as $photo) {

                $user->photos()->detach($photo);
            }
        }
        $user->delete();
        return 'User deleted';
    } catch (Exception $e) {
        dd($e);
    }

Sé que no es automático, pero es muy simple.

Otro enfoque simple sería proporcionar al modelo un método. Me gusta esto:

public function detach(){
       try {

            if ($this->has('photos')) {
                foreach ($this->photos as $photo) {

                    $this->photos()->detach($photo);
                }
            }

        } catch (Exception $e) {
            dd($e);
        }
}

Entonces simplemente puede llamar a esto donde lo necesite:

$user->detach();
$user->delete();
Carlos A. Carneiro
fuente
0

O puede hacer esto si lo desea, solo otra opción:

try {
    DB::connection()->pdo->beginTransaction();

    $photos = Photo::where('user_id', '=', $user_id)->delete(); // Delete all photos for user
    $user = Geofence::where('id', '=', $user_id)->delete(); // Delete users

    DB::connection()->pdo->commit();

}catch(\Laravel\Database\Exception $e) {
    DB::connection()->pdo->rollBack();
    Log::exception($e);
}

Tenga en cuenta que si no está utilizando la conexión de laravel db predeterminada, debe hacer lo siguiente:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();
Darren Powers
fuente
0

Para elaborar la respuesta seleccionada, si sus relaciones también tienen relaciones secundarias que deben eliminarse, primero debe recuperar todos los registros de relaciones secundarias, luego llamar al delete()método para que sus eventos de eliminación también se activen correctamente.

Puede hacerlo fácilmente con mensajes de orden superior .

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get()->each->delete();
        });
    }
}

También puede mejorar el rendimiento consultando solo la columna ID de relaciones:

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get(['id'])->each->delete();
        });
    }
}
Steve Bauman
fuente
-1

sí, pero como @supersan declaró arriba en un comentario, si elimina () en un QueryBuilder, el evento del modelo no se activará, porque no estamos cargando el modelo en sí, y luego llamando a delete () en ese modelo.

Los eventos se activan solo si utilizamos la función de eliminación en una instancia de modelo.

Entonces, este beeing dijo:

if user->hasMany(post)
and if post->hasMany(tags)

para eliminar las etiquetas de publicación al eliminar el usuario, tendríamos que repetir $user->postsy llamar$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> esto activará el evento de eliminación en la publicación

VS

$user->posts()->delete()-> esto no activará el evento de eliminación en la publicación porque en realidad no cargamos el modelo de publicación (solo ejecutamos un SQL como: DELETE * from posts where user_id = $user->idy, por lo tanto, el modelo de publicación ni siquiera se carga)

rechim
fuente
-2

Puede usar este método como alternativa.

Lo que sucederá es que tomamos todas las tablas asociadas con la tabla de usuarios y eliminamos los datos relacionados mediante el bucle

$tables = DB::select("
    SELECT
        TABLE_NAME,
        COLUMN_NAME,
        CONSTRAINT_NAME,
        REFERENCED_TABLE_NAME,
        REFERENCED_COLUMN_NAME
    FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
    WHERE REFERENCED_TABLE_NAME = 'users'
");

foreach($tables as $table){
    $table_name =  $table->TABLE_NAME;
    $column_name = $table->COLUMN_NAME;

    DB::delete("delete from $table_name where $column_name = ?", [$id]);
}
Daanzel
fuente
No creo que todas estas consultas sean necesarias, ya que el elocuente orm puede manejar esto si lo especifica claramente.
7 de