Eliminazione automatica delle righe correlate in Laravel (Eloquent ORM)


158

Quando cancello una riga usando questa sintassi:

$user->delete();

C'è un modo per allegare una sorta di callback, in modo che, ad esempio, lo faccia automaticamente:

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

Preferibilmente all'interno della classe del modello.

Risposte:


205

Credo che questo sia un caso d'uso perfetto per eventi Eloquent ( http://laravel.com/docs/eloquent#model-events ). È possibile utilizzare l'evento "eliminazione" per eseguire la pulizia:

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...
        });
    }
}

Probabilmente dovresti anche inserire tutto all'interno di una transazione, per garantire l'integrità referenziale.


7
Nota: trascorro un po 'di tempo prima che funzioni. Avevo bisogno di aggiungere first()alla query in modo da poter accedere al modello-evento, ad esempio User::where('id', '=', $id)->first()->delete(); Fonte
Michel Ayres,

6
@MichelAyres: sì, devi chiamare delete () su un'istanza del modello, non su Query Builder. Builder ha il suo metodo delete () che praticamente esegue solo una query DELETE sql, quindi presumo che non sappia nulla degli eventi orm ...
ivanhoe

3
Questa è la strada da percorrere per le soft-delete. Credo che il modo Laravel nuovo / preferito sia quello di incollare tutti questi nel metodo boot () di AppServiceProvider in questo modo: \ App \ User :: eleting (function ($ u) {$ u-> photos () -> delete ( );});
Watercayman,

4
Ho lavorato quasi in Laravel 5.5, ho dovuto aggiungere un foreach($user->photos as $photo), quindi $photo->delete()per assicurarmi che ogni bambino avesse i suoi figli rimossi a tutti i livelli, anziché uno solo come stava accadendo per qualche motivo.
George,

9
Questo però non lo fa ulteriormente. Ad esempio, se Photosha tagse fai lo stesso nel Photosmodello (cioè nel deletingmetodo:) $photo->tags()->delete();, non viene mai attivato. Ma se faccio un forciclo e fare qualcosa di simile for($user->photos as $photo) { $photo->delete(); }allora la tagsottiene anche cancellato! solo FYI
sostituire il

200

Puoi effettivamente configurarlo nelle tue migrazioni:

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

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

È inoltre possibile specificare l'azione desiderata per le proprietà "on delete" e "on update" del vincolo:

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

Sì, immagino che avrei dovuto chiarire quella dipendenza.
Chris Schmitz,

62
Ma non se si utilizzano le eliminazioni automatiche, poiché le righe non vengono effettivamente eliminate.
tremby,

7
Inoltre - questo eliminerà il record nel DB, ma non eseguirà il metodo di eliminazione, quindi se si sta facendo un lavoro extra sull'eliminazione (ad esempio - elimina file), non verrà eseguito
amosmos

10
Questo approccio si basa sul fatto che DB esegua l'eliminazione in cascata, ma non tutti i DB lo supportano, quindi è necessario prestare particolare attenzione. Ad esempio MySQL con il motore MyISAM no, né alcun DB NoSQL, SQLite nell'impostazione predefinita, ecc. Ulteriore problema è che l'artigiano non ti avviserà di questo quando esegui le migrazioni, non creerà semplicemente chiavi esterne sulle tabelle MyISAM e quando in seguito cancelli un record non si verificherà alcuna cascata. Ho avuto questo problema una volta e credetemi è molto difficile eseguire il debug.
Ivanhoe,

1
@kehinde L'approccio mostrato dall'utente NON invoca eventi di eliminazione sulle relazioni da eliminare. È necessario scorrere la relazione e chiamare singolarmente Elimina.
Tom,

51

Nota : questa risposta è stata scritta per Laravel 3 . Quindi potrebbe o meno funzionare bene nella versione più recente di Laravel.

È possibile eliminare tutte le foto correlate prima di eliminare effettivamente l'utente.

<?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();
    }
}

Spero che sia d'aiuto.


1
Devi usare: foreach ($ this-> photos come $ photo) ($ this-> photos anziché $ this-> photos ()) Altrimenti, buon consiglio!
Barryvdh,

20
Per renderlo più efficiente, usa una query: Photo :: where ("id_utente", $ this-> id) -> delete (); Non è il modo più bello, ma solo 1 di query, modo migliore performance se un utente ha 1.000.000 photo di.
Dirk,

5
attualmente puoi chiamare: $ this-> photos () -> delete (); non c'è bisogno di loop - ivanhoe
ivanhoe

4
@ivanhoe Ho notato che l'evento di eliminazione non si attiverà nella foto se si elimina la raccolta, tuttavia, iterando come akhyar suggerisce causerà l'attivazione dell'evento di eliminazione. è un insetto?
Adamkrell,

1
@akhyar Quasi, puoi farlo con $this->photos()->delete(). Il photos()restituisce l'oggetto generatore di query.
Sven van Zoelen,

32

Relazione nel modello utente:

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

Elimina record e relativi:

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

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

$user->delete();

4
Funziona, ma fai attenzione a usare $ user () -> relationship () -> detach () se è coinvolta una tabella piviot (in caso di relazioni hasMany / appartieneToMany), altrimenti eliminerai il riferimento, non la relazione .
James Bailey,

Questo funziona per me laravel 6. @Calin puoi spiegarci di più?
Arman H,

20

Ci sono 3 approcci per risolvere questo:

1. Uso degli eventi eloquenti all'avvio del modello (rif: 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. Utilizzo di Eloquent Event Observers (rif: https://laravel.com/docs/5.7/eloquent#observers )

Nel tuo AppServiceProvider, registra l'osservatore in questo modo:

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

Quindi, aggiungi una classe Observer in questo modo:

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

3. Utilizzo dei vincoli di chiave esterna (rif: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

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

1
Penso che le 3 opzioni siano le più eleganti poiché sta costruendo il vincolo nel database stesso. Lo collaudo e funziona benissimo.
Gilbert,

14

A partire da Laravel 5.2, la documentazione afferma che questi tipi di gestori di eventi devono essere registrati in AppServiceProvider:

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

Suppongo persino di spostarli in classi separate anziché in chiusure per una migliore struttura dell'applicazione.


1
Laravel 5.3 consiglia di metterli in classi separate chiamate Osservatori - sebbene sia documentato solo in 5.3, il Eloquent::observe()metodo è disponibile anche in 5.2 e può essere utilizzato da AppServiceProvider.
Leith,

3
Se hai relazioni 'hasMany' dalla tua photos(), dovrai anche stare attento - questo processo lo farà non eliminerà i nipoti perché non stai caricando i modelli. Dovrai eseguire il loop over photos(nota, non photos()) e attivare il delete()metodo su di essi come modelli per attivare gli eventi relativi all'eliminazione.
Leith,

1
@Leith Il metodo observ è disponibile anche in 5.1.
Tyler Reed,

2

È meglio se si ignora il deletemetodo per questo. In questo modo, è possibile incorporare transazioni DB all'interno del deletemetodo stesso. Se usi la modalità evento, dovrai coprire la tua chiamata didelete metodo con una transazione DB ogni volta che la chiami.

Nel tuo Usermodello.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}

1

Nel mio caso è stato piuttosto semplice perché le tabelle del mio database sono InnoDB con chiavi esterne con Cascade su Elimina.

Quindi, in questo caso se la tabella delle foto contiene un riferimento di chiave esterna per l'utente di tutto ciò che devi fare è eliminare l'hotel e la pulizia verrà effettuata dal database, il database eliminerà tutti i record di foto dai dati base.


Come è stato notato in altre risposte, le eliminazioni a cascata a livello di database non funzioneranno quando si utilizzano le eliminazioni automatiche. Compratore stai attento. :)
Ben Johnson,

1

Vorrei scorrere la raccolta staccando tutto prima di eliminare l'oggetto stesso.

ecco un esempio:

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);
    }

So che non è automatico ma è molto semplice.

Un altro approccio semplice sarebbe quello di fornire un modello al modello. Come questo:

public function detach(){
       try {

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

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

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

Quindi puoi semplicemente chiamare questo dove ti serve:

$user->detach();
$user->delete();

0

Oppure puoi farlo se lo desideri, solo un'altra opzione:

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);
}

Nota se non si utilizza la connessione db laravel predefinita, è necessario effettuare le seguenti operazioni:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();

0

Per elaborare la risposta selezionata, se le relazioni hanno anche relazioni secondarie che devono essere eliminate, è necessario prima recuperare tutti i record delle relazioni secondarie, quindi chiamare il delete() metodo in modo che anche i loro eventi di eliminazione vengano generati correttamente.

Puoi farlo facilmente con messaggi di ordine superiore .

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();
        });
    }
}

Puoi anche migliorare le prestazioni eseguendo una query solo sulla colonna ID relazioni:

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();
        });
    }
}

-1

sì, ma come @supersan ha affermato in alto in un commento, se si elimina () su un QueryBuilder, l'evento modello non verrà generato, perché non stiamo caricando il modello stesso, quindi chiamando delete () su quel modello.

Gli eventi vengono generati solo se utilizziamo la funzione di eliminazione su un'istanza del modello.

Quindi, questo beeing ha detto:

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

al fine di eliminare i tag post quando si elimina l'utente, dovremmo scorrere $user->postse chiamare$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> questo genererà l'evento di eliminazione su Post

VS

$user->posts()->delete()-> questo non genererà l'evento di eliminazione sul post perché in realtà non cariciamo il modello di post (eseguiamo solo un SQL come: DELETE * from posts where user_id = $user->ide quindi il modello di post non viene nemmeno caricato)


-2

Puoi usare questo metodo come alternativa.

Quello che accadrà è che prendiamo tutte le tabelle associate alla tabella degli utenti ed eliminiamo i dati relativi usando il loop

$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]);
}

Non credo che tutte queste domande siano necessarie poiché oro eloquente può gestirlo se lo specifichi chiaramente.
7
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.