Secara otomatis menghapus baris terkait di Laravel (Eloquent ORM)

158

Saat saya menghapus baris menggunakan sintaks ini:

$user->delete();

Apakah ada cara untuk melampirkan semacam callback, sehingga misalnya akan melakukan ini secara otomatis:

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

Lebih disukai di dalam kelas model.

Martti Laine
sumber

Jawaban:

205

Saya percaya ini adalah kasus penggunaan yang sempurna untuk acara Eloquent ( http://laravel.com/docs/eloquent#model-events ). Anda dapat menggunakan acara "menghapus" untuk melakukan pembersihan:

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

Anda mungkin juga harus memasukkan semuanya ke dalam transaksi, untuk memastikan integritas referensial ..

ivanhoe
sumber
7
Catatan: Saya meluangkan waktu sampai pekerjaan ini berhasil. Saya perlu menambahkan first()ke dalam query sehingga saya bisa mengakses model-event misalnya User::where('id', '=', $id)->first()->delete(); Sumber
Michel Ayres
6
@MichelAyres: ya, Anda perlu memanggil delete () pada contoh model, bukan pada Query Builder. Builder memiliki metode delete () sendiri yang pada dasarnya hanya menjalankan kueri DELETE sql, jadi saya kira tidak tahu apa-apa tentang acara orm ...
ivanhoe
3
Ini adalah cara untuk menghapus lunak. Saya percaya cara Laravel baru / yang disukai adalah untuk tetap semua ini dalam metode AppServiceProvider boot () dengan cara ini: \ App \ User :: menghapus (fungsi ($ u) {$ u-> foto () -> delete ( );});
Watercayman
4
Hampir bekerja di Laravel 5.5, saya harus menambahkan foreach($user->photos as $photo), kemudian $photo->delete()untuk memastikan setiap anak memiliki anak-anak dihapus pada semua tingkatan, bukan hanya satu seperti yang terjadi karena suatu alasan.
George
9
Ini tidak membuatnya lebih lanjut. Sebagai contoh jika Photosmemiliki tagsdan Anda melakukan hal yang sama dalam Photosmodel (yaitu pada deletingmetode $photo->tags()->delete();:) tidak pernah mendapat pemicu. Tetapi jika saya membuatnya forlingkaran dan melakukan sesuatu seperti for($user->photos as $photo) { $photo->delete(); }maka tagsjuga bisa dihapus! just FYI
supersan
200

Anda sebenarnya dapat mengatur ini di migrasi Anda:

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

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

Anda juga dapat menentukan tindakan yang diinginkan untuk properti "saat penghapusan" dan "saat pembaruan" dari kendala:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
sumber
Ya, saya kira saya harus menjelaskan ketergantungan itu.
Chris Schmitz
62
Tetapi tidak jika Anda menggunakan penghapusan lunak, karena baris tidak benar-benar dihapus.
gemetar
7
Juga - ini akan menghapus catatan dalam DB, tetapi tidak akan menjalankan metode hapus Anda, jadi jika Anda melakukan pekerjaan tambahan pada delete (misalnya - hapus file), itu tidak akan berjalan
amosmos
10
Pendekatan ini bergantung pada DB untuk melakukan penghapusan kaskade, tetapi tidak semua DB mendukung hal ini, sehingga diperlukan perhatian ekstra. Misalnya MySQL dengan mesin MyISAM tidak, atau DB NoSQL, SQLite di pengaturan default, dll. Masalah tambahan adalah bahwa pengrajin tidak akan memperingatkan Anda tentang hal ini ketika Anda menjalankan migrasi, itu tidak akan membuat kunci asing pada tabel MyISAM dan saat nanti Anda menghapus catatan, kaskade tidak akan terjadi. Saya pernah mengalami masalah ini dan percaya bahwa sangat sulit untuk melakukan debug.
ivanhoe
1
@kehinde Pendekatan yang ditunjukkan oleh Anda TIDAK meminta penghapusan acara pada relasi yang akan dihapus. Anda harus mengulangi hubungan dan panggilan hapus satu per satu.
Tom
51

Catatan : Jawaban ini ditulis untuk Laravel 3 . Dengan demikian mungkin atau mungkin tidak berfungsi dengan baik di versi Laravel yang lebih baru.

Anda dapat menghapus semua foto terkait sebelum benar-benar menghapus pengguna.

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

Semoga ini bisa membantu.

akhy
sumber
1
Anda harus menggunakan: foreach ($ this-> foto sebagai $ photo) ($ this-> foto, bukan $ this-> foto ()) Jika tidak, tip yang bagus!
Barryvdh
20
Untuk membuatnya lebih efisien, gunakan satu permintaan: Foto :: where ("user_id", $ this-> id) -> delete (); Bukan cara terbaik, tetapi hanya 1 permintaan, kinerja jauh lebih baik jika pengguna memiliki 1.000.000 foto.
Dirk
5
sebenarnya Anda dapat menelepon: $ this-> photos () -> delete (); tidak perlu untuk loop - ivanhoe
ivanhoe
4
@ivanhoe Saya perhatikan bahwa acara penghapusan tidak akan diaktifkan di foto jika Anda menghapus koleksi, namun, iterasi melalui seperti yang disarankan akhyar akan menyebabkan acara penghapusan menjadi api. Apakah ini bug?
adamkrell
1
@akhyar Hampir, Anda bisa melakukannya dengan $this->photos()->delete(). The photos()mengembalikan objek query builder.
Sven van Zoelen
32

Hubungan dalam model Pengguna:

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

Hapus rekaman dan yang terkait:

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

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

$user->delete();
Calin Blaga
sumber
4
Ini berfungsi, tetapi berhati-hatilah menggunakan $ user () -> relation () -> detach () jika ada tabel piviot yang terlibat (dalam kasus hubungan hasMany / hersToMany), atau Anda akan menghapus referensi, bukan relasi .
James Bailey
Ini berfungsi untuk saya laravel 6. @Calin dapat Anda jelaskan lebih banyak pls?
Arman H
20

Ada 3 pendekatan untuk menyelesaikan ini:

1. Menggunakan Eloquent Events Pada Model Boot (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. Menggunakan Pengamat Acara Eloquent (ref: https://laravel.com/docs/5.7/eloquent#observers )

Di AppServiceProvider Anda, daftarkan pengamat seperti:

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

Selanjutnya, tambahkan kelas Observer seperti:

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

3. Menggunakan Batasan Kunci Asing (ref: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
sumber
1
Saya berpikir bahwa 3 opsi adalah yang paling elegan karena membangun kendala ke dalam database itu sendiri. Saya mengujinya dan bekerja dengan baik.
Gilbert
14

Pada Laravel 5.2, dokumentasi menyatakan bahwa jenis penangan acara ini harus terdaftar di AppServiceProvider:

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

Saya bahkan berpikir untuk memindahkan mereka ke kelas yang terpisah, bukan penutup untuk struktur aplikasi yang lebih baik.

Attila Fulop
sumber
1
Laravel 5.3 merekomendasikan untuk menempatkan mereka di kelas terpisah yang disebut Pengamat - sementara itu hanya didokumentasikan dalam 5.3, Eloquent::observe()metode ini tersedia dalam 5.2 juga dan dapat digunakan dari AppServiceProvider.
Leith
3
Jika Anda memiliki hubungan 'hasMany' dari Anda photos(), Anda juga harus berhati-hati - proses ini tidak akan menghapus cucu karena Anda tidak memuat model. Anda harus mengulang photos(perhatikan, tidak photos()) dan jalankan delete()metode pada mereka sebagai model untuk memecat peristiwa terkait penghapusan.
Leith
1
@Leith Metode observasi juga tersedia dalam 5.1.
Tyler Reed
2

Lebih baik jika Anda mengganti deletemetode untuk ini. Dengan begitu, Anda bisa memasukkan transaksi DB dalam deletemetode itu sendiri. Jika Anda menggunakan cara acara, Anda harus menutup panggilandelete metode Anda dengan transaksi DB setiap kali Anda menyebutnya.

Dalam Usermodel Anda .

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
sumber
1

Dalam kasus saya itu cukup sederhana karena tabel database saya adalah InnoDB dengan kunci asing dengan Cascade on Delete.

Jadi dalam hal ini jika tabel foto Anda berisi referensi kunci asing untuk pengguna daripada yang harus Anda lakukan adalah menghapus hotel dan pembersihan akan dilakukan oleh Pangkalan Data, basis data akan menghapus semua catatan foto dari data tersebut. mendasarkan.

Alex
sumber
Seperti yang telah dicatat dalam Jawaban lain, penghapusan cascading di lapisan database tidak akan berfungsi saat menggunakan Soft Deletes. Hati-hati pembeli. :)
Ben Johnson
1

Saya akan beralih melalui koleksi yang memisahkan semuanya sebelum menghapus objek itu sendiri.

ini sebuah contoh:

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

Saya tahu ini tidak otomatis tetapi sangat sederhana.

Pendekatan sederhana lain adalah menyediakan model dengan metode. Seperti ini:

public function detach(){
       try {

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

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

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

Maka Anda cukup memanggil ini di tempat yang Anda butuhkan:

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

Atau Anda dapat melakukan ini jika mau, hanya opsi lain:

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

Catatan jika Anda tidak menggunakan koneksi default laravel db maka Anda perlu melakukan hal berikut:

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

Untuk menguraikan jawaban yang dipilih, jika hubungan Anda juga memiliki hubungan anak yang harus dihapus, Anda harus mengambil semua catatan hubungan anak terlebih dahulu, kemudian memanggil delete() metode tersebut sehingga peristiwa penghapusannya juga dipecat dengan benar.

Anda dapat melakukan ini dengan mudah dengan pesan tingkat tinggi .

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

Anda juga dapat meningkatkan kinerja dengan hanya menanyakan kolom ID hubungan:

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

ya, tetapi seperti yang dikatakan @supersan di bagian atas dalam komentar, jika Anda menghapus () di QueryBuilder, acara model tidak akan diaktifkan, karena kami tidak memuat model itu sendiri, lalu memanggil delete () pada model itu.

Peristiwa dipecat hanya jika kita menggunakan fungsi hapus pada Instance Model.

Jadi, beeing ini berkata:

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

untuk menghapus tag posting ketika menghapus pengguna, kita harus mengulang $user->postsdan menelepon$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> ini akan memecat acara penghapusan di Posting

VS

$user->posts()->delete()-> ini tidak akan mem-boot event penghapusan pada postingan karena kita sebenarnya tidak memuat Post Model (kita hanya menjalankan SQL like: DELETE * from posts where user_id = $user->iddan dengan demikian, model Post bahkan tidak dimuat)

rechim
sumber
-2

Anda dapat menggunakan metode ini sebagai alternatif.

Apa yang akan terjadi adalah bahwa kami mengambil semua tabel yang terkait dengan tabel pengguna dan menghapus data terkait menggunakan perulangan

$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
sumber
Saya tidak berpikir semua pertanyaan ini diperlukan karena orm fasih dapat menangani ini jika Anda menentukannya dengan jelas.
7