Verwandte Zeilen in Laravel automatisch löschen (Eloquent ORM)

158

Wenn ich eine Zeile mit dieser Syntax lösche:

$user->delete();

Gibt es eine Möglichkeit, eine Art Rückruf anzuhängen, so dass dies z. B. automatisch geschieht:

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

Am besten innerhalb der Modellklasse.

Martti Laine
quelle

Antworten:

203

Ich glaube, dies ist ein perfekter Anwendungsfall für eloquente Ereignisse ( http://laravel.com/docs/eloquent#model-events ). Sie können das Ereignis "Löschen" verwenden, um die Bereinigung durchzuführen:

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

Sie sollten wahrscheinlich auch das Ganze in eine Transaktion einfügen, um die referenzielle Integrität sicherzustellen.

ivanhoe
quelle
7
Hinweis: Ich verbringe einige Zeit, bis dies funktioniert. Ich musste first()der Abfrage hinzufügen, damit ich auf das Modellereignis zugreifen konnte, z. B. User::where('id', '=', $id)->first()->delete(); Quelle
Michel Ayres,
6
@MichelAyres: Ja, Sie müssen delete () für eine Modellinstanz aufrufen, nicht für Query Builder. Builder hat eine eigene delete () -Methode, die im Grunde nur eine DELETE-SQL-Abfrage ausführt, also nehme ich an, dass sie nichts über orm-Ereignisse weiß ...
ivanhoe
3
Dies ist der richtige Weg für Soft-Deletes. Ich glaube, der neue / bevorzugte Laravel-Weg besteht darin, all dies auf folgende Weise in die boot () -Methode des AppServiceProvider zu integrieren: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
Watercayman
4
Fast in Laravel 5.5 gearbeitet, musste ich ein hinzufügen foreach($user->photos as $photo), $photo->delete()um sicherzustellen, dass jedes Kind seine Kinder auf allen Ebenen entfernt hat, anstatt nur eines, wie es aus irgendeinem Grund geschah.
George
9
Dies kaskadiert es jedoch nicht weiter. Wenn zum Beispiel Photoshas tagsund Sie dasselbe im PhotosModell tun (dh bei deletingmethod :), wird $photo->tags()->delete();es nie ausgelöst. Aber wenn ich es zu einer forSchleife mache und so etwas mache, werden for($user->photos as $photo) { $photo->delete(); }die tagsauch gelöscht! nur zu
Ihrer Information
198

Sie können dies tatsächlich in Ihren Migrationen einrichten:

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

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

Sie können auch die gewünschte Aktion für die Eigenschaften "Beim Löschen" und "Beim Aktualisieren" der Einschränkung angeben:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
quelle
Ja, ich denke, ich hätte diese Abhängigkeit klären sollen.
Chris Schmitz
62
Aber nicht, wenn Sie weiche Löschvorgänge verwenden, da die Zeilen dann nicht wirklich gelöscht werden.
Tremby
7
Außerdem - dies löscht den Datensatz in der Datenbank, führt jedoch Ihre Löschmethode nicht aus. Wenn Sie also zusätzliche Arbeiten zum Löschen ausführen (z. B. Dateien löschen), wird er nicht ausgeführt
amosmos
10
Dieser Ansatz basiert auf DB, um das Löschen der Kaskade durchzuführen, aber nicht alle DBs unterstützen dies, sodass zusätzliche Sorgfalt erforderlich ist. Zum Beispiel MySQL mit MyISAM-Engine nicht, noch NoSQL-DBs, SQLite im Standard-Setup usw. Ein weiteres Problem ist, dass Handwerker Sie nicht davor warnen, wenn Sie Migrationen ausführen, sondern nur keine Fremdschlüssel in MyISAM-Tabellen und erstellen Wenn Sie später einen Datensatz löschen, tritt keine Kaskade auf. Ich hatte dieses Problem einmal und glaube mir, es ist sehr schwer zu debuggen.
Iwanhoe
1
@kehinde Der von Ihnen gezeigte Ansatz ruft KEINE Löschereignisse für die zu löschenden Beziehungen auf. Sie sollten die Beziehung durchlaufen und delete einzeln aufrufen.
Tom
51

Hinweis : Diese Antwort wurde für Laravel 3 geschrieben . So könnte oder könnte nicht gut in neueren Version von Laravel funktioniert.

Sie können alle zugehörigen Fotos löschen, bevor Sie den Benutzer tatsächlich löschen.

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

Ich hoffe es hilft.

akhy
quelle
1
Sie müssen verwenden: foreach ($ this-> Fotos als $ Foto) ($ this-> Fotos anstelle von $ this-> Fotos ()) Ansonsten guter Tipp!
Barryvdh
20
Verwenden Sie eine Abfrage, um die Effizienz zu steigern: Photo :: where ("user_id", $ this-> id) -> delete (); Nicht der schönste Weg, aber nur 1 Abfrage, viel bessere Leistung, wenn ein Benutzer 1.000.000 Fotos hat.
Dirk
5
Eigentlich kann man anrufen: $ this-> photos () -> delete (); keine Notwendigkeit für Schleife - ivanhoe
ivanhoe
4
@ivanhoe Ich habe festgestellt, dass das Löschereignis im Foto nicht ausgelöst wird, wenn Sie die Sammlung löschen. Wenn Sie jedoch wie von akhyar vorgeschlagen durchlaufen, wird das Löschereignis ausgelöst. Ist das ein Fehler?
Adamkrell
1
@akhyar Fast können Sie es mit tun $this->photos()->delete(). Das photos()gibt das Query Builder-Objekt zurück.
Sven van Zoelen
32

Beziehung im Benutzermodell:

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

Datensatz löschen und verwandte:

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

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

$user->delete();
Calin Blaga
quelle
4
Dies funktioniert, aber verwenden Sie vorsichtig $ user () -> Relation () -> Detach (), wenn es sich um eine Piviot-Tabelle handelt (im Fall von hasMany / GehorsToMany-Relationen). Andernfalls löschen Sie die Referenz und nicht die Relation .
James Bailey
Dies funktioniert für mich Laravel 6. @Calin können Sie mehr pls erklären?
Arman H
19

Es gibt 3 Lösungsansätze:

1. Verwenden eloquenter Ereignisse beim Modellstart (siehe: 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. Verwenden von eloquenten Ereignisbeobachtern (siehe: https://laravel.com/docs/5.7/eloquent#observers )

Registrieren Sie den Beobachter in Ihrem AppServiceProvider wie folgt:

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

Fügen Sie als Nächstes eine Observer-Klasse wie folgt hinzu:

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

3. Verwenden von Fremdschlüsseleinschränkungen (siehe: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
quelle
1
Ich denke, dass die 3 Optionen die elegantesten sind, da die Einschränkung in die Datenbank selbst eingebaut wird. Ich teste es und funktioniert einwandfrei.
Gilbert
14

Ab Laravel 5.2 heißt es in der Dokumentation, dass diese Art von Ereignishandlern im AppServiceProvider registriert werden sollten:

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

Ich nehme sogar an, sie für eine bessere Anwendungsstruktur in separate Klassen anstatt in Schließungen zu verschieben.

Attila Fulop
quelle
1
Laravel 5.3 empfiehlt, sie in separate Klassen mit dem Namen Observers einzuteilen. Obwohl dies nur in 5.3 dokumentiert ist, ist die Eloquent::observe()Methode auch in 5.2 verfügbar und kann über den AppServiceProvider verwendet werden.
Leith
3
Wenn Sie irgendwelche 'hasMany'-Beziehungen von Ihnen haben photos(), müssen Sie auch vorsichtig sein - dieser Prozess löscht keine Enkelkinder, da Sie keine Modelle laden. Sie müssen eine Schleife durchführen photos(beachten Sie, nicht photos()) und die delete()Methode auf ihnen als Modelle auslösen, um die löschbezogenen Ereignisse auszulösen.
Leith
1
@Leith Die Beobachtungsmethode ist auch in 5.1 verfügbar.
Tyler Reed
2

Es ist besser, wenn Sie die deleteMethode dafür überschreiben . Auf diese Weise können Sie DB-Transaktionen in die deleteMethode selbst integrieren. Wenn Sie die Ereignismethode verwenden, müssen Sie Ihren deleteMethodenaufruf bei jedem Aufruf mit einer DB-Transaktion abdecken .

In Ihrem UserModell.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
quelle
1

In meinem Fall war es ziemlich einfach, weil meine Datenbanktabellen InnoDB mit Fremdschlüsseln mit Cascade on Delete sind.

Wenn Ihre Fototabelle in diesem Fall eine Fremdschlüsselreferenz für den Benutzer enthält, müssen Sie lediglich das Hotel löschen und die Bereinigung durch die Datenbank durchführen. Die Datenbank löscht alle Fotodatensätze aus den Daten Base.

Alex
quelle
Wie bereits in anderen Antworten erwähnt, funktionieren kaskadierende Löschvorgänge auf Datenbankebene bei Verwendung von Soft Deletes nicht. Käufer aufgepasst. :)
Ben Johnson
1

Ich würde die Sammlung durchlaufen und alles trennen, bevor ich das Objekt selbst lösche.

Hier ist ein Beispiel:

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

Ich weiß, dass es nicht automatisch ist, aber es ist sehr einfach.

Ein anderer einfacher Ansatz wäre, dem Modell eine Methode zur Verfügung zu stellen. So was:

public function detach(){
       try {

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

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

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

Dann können Sie dies einfach dort anrufen, wo Sie es brauchen:

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

Oder Sie können dies tun, wenn Sie möchten, nur eine weitere Option:

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

Hinweis: Wenn Sie nicht die Standard-Laravel-Datenbankverbindung verwenden, müssen Sie folgende Schritte ausführen:

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

Um auf die ausgewählte Antwort einzugehen, müssen Sie, wenn Ihre Beziehungen auch untergeordnete Beziehungen haben, die gelöscht werden müssen, zuerst alle untergeordneten Beziehungsdatensätze abrufen und dann die delete()Methode aufrufen , damit auch ihre Löschereignisse ordnungsgemäß ausgelöst werden.

Sie können dies problemlos mit Nachrichten höherer Ordnung tun .

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

Sie können die Leistung auch verbessern, indem Sie nur die Spalte Beziehungs-ID abfragen:

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
quelle
-1

Ja, aber wie @supersan oben in einem Kommentar angegeben hat, wird das Modellereignis nicht ausgelöst, wenn Sie () in einem QueryBuilder löschen (), da wir das Modell selbst nicht laden und dann delete () für dieses Modell aufrufen.

Die Ereignisse werden nur ausgelöst, wenn wir die Löschfunktion für eine Modellinstanz verwenden.

Also sagte dieses Wesen:

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

Um die Post-Tags beim Löschen des Benutzers zu löschen, müssten wir iterieren $user->postsund aufrufen$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> Dadurch wird das Löschereignis auf Post ausgelöst

VS

$user->posts()->delete()-> Dadurch wird das Löschereignis beim Posten nicht ausgelöst, da das Post-Modell nicht tatsächlich geladen wird (wir führen nur eine SQL wie DELETE * from posts where user_id = $user->idfolgt aus : Daher wird das Post-Modell nicht einmal geladen).

rechim
quelle
-2

Sie können diese Methode alternativ verwenden.

Was passieren wird, ist, dass wir alle mit der Benutzertabelle verknüpften Tabellen nehmen und die zugehörigen Daten mithilfe von Schleifen löschen

$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
quelle
Ich denke nicht, dass all diese Abfragen notwendig sind, da eloquent orm damit umgehen kann, wenn Sie es klar spezifizieren.
7rust