Автоматичне видалення пов'язаних рядків у Laravel (Eloquent ORM)


158

Коли я видаляю рядок за допомогою цього синтаксису:

$user->delete();

Чи є спосіб приєднати такий тип зворотного виклику, щоб, наприклад, це зробити автоматично:

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

Переважно всередині модельного класу.

Відповіді:


205

Я вважаю, що це ідеальний варіант використання для красномовних подій ( http://laravel.com/docs/eloquent#model-events ). Ви можете скористатися подією "видалення", щоб зробити очищення:

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

Ви, ймовірно, також повинні помістити всю річ всередину транзакції, щоб забезпечити цілісність референції.


7
Примітка. Я витрачаю деякий час, поки я не працюю над цим. Мені потрібно було додати first()запит, щоб я міг отримати доступ до події моделі, наприклад User::where('id', '=', $id)->first()->delete(); Джерело
Мішель Айрес

6
@MichelAyres: так, вам потрібно зателефонувати delete () у примірнику моделі, а не в Builder запитів. У Builder є власний метод delete (), який в основному просто виконує запит DELETE sql, тому я припускаю, що він нічого не знає про події в orm ...
ivanhoe

3
Це шлях для Soft-Deletes. Я вважаю, що новий / кращий спосіб Laravel полягає в склеюванні всього цього в методі boS () AppServiceProvider таким чином: \ App \ User :: видалення (функція ($ u) {$ u-> photos () -> delete ( );});
Watercayman

4
Майже працював у Laravel 5.5, мені довелося додати foreach($user->photos as $photo), $photo->delete()щоб переконатися, що кожна дитина видалила своїх дітей на всіх рівнях, а не лише одного, як це відбувалося з якихось причин.
Джордж

9
Це, однак, не каскадує його далі. Наприклад, якщо Photosє tagsі ви робите те ж саме в Photosмоделі (тобто за deletingметодом $photo->tags()->delete();:), він ніколи не отримує тригер. Але якщо я зроблю це forцикл і роблю щось подібне, for($user->photos as $photo) { $photo->delete(); }то tagsтакож видаляю! просто FYI
supersan

199

Ви можете фактично налаштувати це під час міграцій:

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

Джерело: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

Ви також можете вказати бажану дію для властивостей обмеження "при видаленні" та "оновлення":

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

Так, я думаю, я мав би прояснити цю залежність.
Кріс Шмітц

62
Але не, якщо ви використовуєте м'які видалення, оскільки рядки не видаляються.
тремтій

7
Також - це видалить запис у БД, але не запустить ваш метод видалення, тому якщо ви робите додаткову роботу над видаленням (наприклад - видалення файлів), він не запуститься
amosmos

10
Цей підхід покладається на БД для каскадного видалення, але не всі БД підтримують це, тому потрібен додатковий догляд. Наприклад, MySQL з двигуном MyISAM не має, ні будь-які БД NoSQL, SQLite в налаштуваннях за замовчуванням і т.д. коли ви видалите запис, каскад не відбудеться. У мене була ця проблема колись, і повірте, налагодити це дуже важко.
ivanhoe

1
@kehinde Підхід, показаний вами, НЕ викликає подій видалення на відносини, які слід видалити. Ви повинні повторити зв'язок і видалити виклик окремо.
Том

51

Примітка . Ця відповідь була написана для Laravel 3 . Таким чином, це може чи не може спрацювати добре в більш новій версії Laravel.

Ви можете видалити всі пов’язані фотографії, перш ніж фактично видалити користувача.

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

Сподіваюся, це допомагає.


1
Ви повинні використовувати: foreach ($ this-> фотографії як $ photo) ($ this-> фотографії замість $ this-> photos ()) Інакше, хороша порада!
Barryvdh

20
Щоб зробити його більш ефективним, використовуйте один запит: Photo :: where ("user_id", $ this-> id) -> delete (); Чи не найкращий спосіб, але тільки один запит, спосіб більш високу продуктивність , якщо користувач має 1.000.000 фотографії.
Дірк

5
насправді ви можете зателефонувати: $ this-> photos () -> delete (); не потрібно петлі - ivanhoe
ivanhoe

4
@ivanhoe Я помітив, що подія видалення не запустить фотографію, якщо ви видалите колекцію, однак, повторення, як пропонує akhyar, призведе до запуску події видалення. Це помилка?
adamkrell

1
@akhyar Майже ви можете це зробити $this->photos()->delete(). photos()Повертає об'єкт конструктор запитів.
Свен ван Зоелен

32

Відношення в моделі користувача:

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

Видалити запис і пов’язане з ним:

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

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

$user->delete();

4
Це працює, але використовуйте обережно, щоб використовувати $ user () -> relation () -> detach (), якщо є таблиця piviot (у випадку відносин hasMany / pripadaToMany), інакше ви видалите посилання, а не відношення .
Джеймс Бейлі

Це працює для мене laravel 6. @Calin Ви можете пояснити більше pls?
Арман Н

19

Існує 3 підходи до вирішення цього питання:

1. Використання красномовних подій у завантаженні моделі (посилання: 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. Використання красномовних спостерігачів подій (посилання: https://laravel.com/docs/5.7/eloquent#observers )

У своєму AppServiceProvider зареєструйте спостерігача так:

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

Далі додайте клас спостерігача так:

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

3. Використання зовнішніх ключових обмежень (див. Https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

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

1
Я думаю, що три варіанти є найелегантнішими, оскільки будує обмеження в самій базі даних. Я тестую це і працює чудово.
Гілберт

14

Станом на Laravel 5.2, у документації зазначено, що такі види обробників подій повинні бути зареєстровані в AppServiceProvider:

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

Я навіть припускаю перемістити їх у окремі класи замість закриття для кращої структури додатків.


1
Laravel 5.3 рекомендує розмістити їх в окремих класах, які називаються спостерігачами - хоча це документально зафіксовано лише в 5.3, але Eloquent::observe()метод доступний і в 5.2, і його можна використовувати з AppServiceProvider.
Лейт

3
Якщо у вас є якісь стосунки "hasMany" зі своїми photos(), вам також потрібно бути обережними - цей процес не видалить онуків, оскільки ви не завантажуєте моделі. Вам потрібно буде переосмислити photos(зверніть увагу, не photos()) та запустити delete()метод на них як моделі, щоб запустити події, пов’язані з видаленням.
Лейт

1
@Leith Спосіб спостереження також доступний у 5.1.
Тайлер Рід

2

Краще, якщо ви перекриєте deleteметод для цього. Таким чином, ви можете включати транзакції з БД у сам deleteметод. Якщо ви використовуєте спосіб події, вам доведеться покривати свій виклик deleteметоду транзакцією DB кожного разу, коли ви його викликаєте.

У вашій Userмоделі.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}

1

У моєму випадку це було досить просто, тому що в моїх базах даних є InnoDB із зовнішніми ключами із Cascade on Delete.

Тож у такому випадку, якщо таблиця ваших фотографій містить користувальницьку довідкову інформацію, ніж все, що вам потрібно зробити, це видалити готель, а очищення буде здійснено за допомогою Бази даних, база даних видалить усі записи фотографій із даних база.


Як було відмічено в інших відповідях, каскадні видалення на рівні бази даних не працюватимуть при використанні програмного забезпечення програмного видалення. Покупця остерігайтесь. :)
Бен Джонсон

1

Я хотів би повторити колекцію, де деталізується все, перш ніж видаляти сам об'єкт.

ось приклад:

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

Я знаю, що це не автоматично, але це дуже просто.

Іншим простим підходом було б надання моделі методом. Подобається це:

public function detach(){
       try {

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

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

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

Тоді ви можете просто зателефонувати цьому, де вам потрібно:

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

0

Або ви можете зробити це, якщо хочете, просто інший варіант:

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

Зверніть увагу, якщо ви не використовуєте підключення db за замовчуванням, тоді вам потрібно зробити наступне:

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

0

Щоб уточнити обрану відповідь, якщо у ваших стосунках також є стосунки з дітьми, які потрібно видалити, спочатку потрібно отримати всі записи про стосунки з дітьми, а потім зателефонувати за delete()методом, щоб і їх події видалення також були належними.

Це можна легко зробити з повідомленнями вищого порядку .

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

Ви також можете покращити ефективність, запитуючи лише стовпчик ідентифікаторів відносин:

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

так, але, як @supersan зазначив верх у коментарі, якщо ви видалите () на QueryBuilder, подія моделі не буде запущено, оскільки ми не завантажуємо саму модель, а потім викликаємо delete () на цій моделі.

Події запускаються, лише якщо ми використовуємо функцію видалення на екземплярі моделі.

Отже, цей бджіл сказав:

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

щоб видалити теги допису під час видалення користувача, нам доведеться повторити $user->postsта дзвонити$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> це призведе до запуску події видалення в Пості

VS

$user->posts()->delete()-> це не призведе до запуску події видалення на посаді, оскільки ми фактично не завантажуємо модель Post (ми запускаємо лише SQL типу: DELETE * from posts where user_id = $user->idі, таким чином, модель Post навіть не завантажується)


-2

Ви можете використовувати цей метод як альтернативу.

Що станеться, ми беремо всі таблиці, пов’язані з таблицею користувачів, і видаляємо пов’язані дані за допомогою циклу

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

Я не думаю, що всі ці запити не потрібні, оскільки красномовний орм може впоратися з цим, якщо ви чітко вказали його.
грудня 19
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.