Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eloquent Model delete/deleting events are not triggered on a where(...)->delete() #2536

Closed
maximerassi opened this issue Oct 22, 2013 · 26 comments

Comments

@maximerassi
Copy link

Scenario A
Analytic::where('id', 2)->delete();

  • this deletes the entry in the database
  • this does not trigger the deleting/deleted events on the model

Scenario B
Analytic::where('id', 2)->first()->delete();

  • this deletes the entry in the database
  • this does trigger the deleting/deleted events on the model

Is this expected behavior?

public static function boot() {
    static::deleting(function($obj) {
    });
    static::deleted(function($obj) {
    });
}

referring doc: http://four.laravel.com/docs/eloquent

@taylorotwell
Copy link
Member

Yes, this is basically expected. In order to fire the events we would have to pull the models first and then call the events with the model instances. Then call the delete query.

orrd pushed a commit to orrd/framework that referenced this issue Jan 17, 2014
Issue laravel#2536 fixed an issue where URL::to()  couldn't be used to create
http URLs from an https page.  This is a fix to a similar but different
issue where URL::route() also can't be used to create routes that are
http if the current page is https.

In order to properly fix this issue with consistency for Laravel route
design, it was necessary to add an "http" route option (an "https"
option already existed, but there was no "http" option).

So with this commit, it is now possible to specify 'http' in a route
declaration, which behaves exactly as the 'https' option does, forcing
generated route URLs to use http, and only responding to http requests
for the route.
orrd pushed a commit to orrd/framework that referenced this issue Jan 17, 2014
Issue laravel#2536 fixed an issue where URL::to() couldn't be used to create
http URLs from an https page.  This is a fix to a similar but different
issue where URL::route() can't be used to create routes to URLs that are
http if the current page is https.

In order to properly fix this issue with consistency for the current
route design, it was necessary to add an 'http' option to the route
actions (an 'https' option already existed).

So with this commit it is now possible to specify 'http' in a route
declaration, which behaves exactly as the 'https' option does, forcing
generated route URLs to use http, and only responding to http requests
for that route.
@w3z315
Copy link

w3z315 commented Feb 16, 2015

Is there a workaround?

@orrd
Copy link

orrd commented Feb 16, 2015

Just to be clear, I wouldn't say you need a "workaround" since this isn't a bug, it really does make sense when you understand what's going on. The solution you need to use instead depends on what you're trying to do.

First, let's talk about their first example:

Analytic::where('id', 2)->delete();

All this actually does is execute SQL, probably something like "DELETE FROM analytic WHERE id=2". That's it. It doesn't load the models into memory, etc. For these kinds of functions, Laravel is just passing control to an underlying Query Builder query that performs the actions in SQL. That's just something that Eloquent lets you do, it lets you perform Query actions directly on its underlying query object.

In order to trigger events when you delete something, the Eloquent model would have to be loaded into memory, and then delete() has to be called on each model individually. This is an entirely different kind of operation. The question gives you an example of how to do that for one object with the ID "2":

Analytic::where('id', 2)->first()->delete();

In this case "first()" loads the model into memory, and then the delete() function is called on it. If you have more than one row/object that you want to delete at a time, you can do something like this (be careful with the where() statements, you don't want to delete things you don't want to!):

$analytics = Analytic::where('id', '>', 100)->get();
foreach ($analytics as $analytic) $analytic->delete();

Here's another way to do the same thing using Laravel's each() function that it gives you for working on Collections...

Analytic::where('id', '>', 100)->get()->each(function($analytic) {
    $analytic->delete();
});

@silverdr
Copy link

I have something similar and still consider it somewhat confusing to say the least.

I have a typical event hook for soft deleting children:

public static function boot()
{
    parent::boot();
    static::deleted(function($model1)
    {
        $model1->hasmanyrelation()->delete();
    });
}

and

public function hasmanyrelation()
{
    return $this->hasMany('Model2');
}

Now when I use:

$model0->model1->each(function($model1){$model1->delete();});

things work as expected and model2 children of model 1 get (soft) deleted. But when I use:

$model0->model1()->delete();

then all related model1 records get deleted but model2 and all its records remain untouched.

@alihossein
Copy link

thank u my problem is fix

@rahilwazir
Copy link

@orrd Thanks for explanation!

@orrd
Copy link

orrd commented Sep 15, 2015

@silverdr, yes that is the expected behavior. When you call $model0->model1()->delete(); you are only executing SQL, so your delete events never get triggered. The way Laravel relations work, $model0->model1() pretty much just gets you an Query Builder class, so you can use any of the Query Builder functions on it. For example $model0->model1()->where('foo', '=', 'bar')->get().

That's different than $model0->model1 (without parenthesis on model1). That does something completely different. That fetches your model1 objects from the database and gives them to you in a Collection. That's why when you call delete() on each one, your delete events get triggered.

Understanding the difference between $model0->model1() and $model0->model1 is the key to understanding Laravel relations (personally I didn't even notice the difference for the first year or so of using Laravel, but everything made a lot more sense once I understood that).

@ameoba32
Copy link

ameoba32 commented Oct 1, 2015

The Law of Leaky Abstractions

@lukepolo
Copy link
Contributor

@really i don't see why we can fire events from the builder, since we have all the necessary functionality. For now I have extended the builder class, added this function

    protected function fireModelEvent($event, $halt = true)
    {
        if(empty(!$this->getModel())) {
              $dispatcher = $this->query->getConnection()->getEventDispatcher();

              if (empty($dispatcher)) {
                  return true;
              }

              // We will append the names of the class to the event to distinguish it from
              // other model events that are fired, allowing us to listen on each model
              // event set individually instead of catching event for all the models.
              // event set individually instead of catching event for all the models.
              $event = "eloquent.{$event}: ".get_class($this->getModel());

              $method = $halt ? 'until' : 'fire';

              return $dispatcher->$method($event, $this);
        }

    }

Then add the proper $this->fireModelEvent('deleted', false) , and $this->fireModelEvent('save', false)

Let me know what you think

@antonkomarev
Copy link
Contributor

+1 for solving this issue.

Details where I found this useful.

Why it isn't working is logical, but not obvious. At least this should be documented in Query builder section.

@rico-ocepek
Copy link

Btw in 2018 you can replace
Analytic::where('id', '>', 100)->get()->each(function($analytic) { $analytic->delete(); });

with
Analytic::where('id', '>', 100)->get()->each->delete();

@gtl-dhwani
Copy link

@silverdr, yes that is the expected behavior. When you call $model0->model1()->delete(); you are only executing SQL, so your delete events never get triggered. The way Laravel relations work, $model0->model1() pretty much just gets you an Query Builder class, so you can use any of the Query Builder functions on it. For example $model0->model1()->where('foo', '=', 'bar')->get().

That's different than $model0->model1 (without parenthesis on model1). That does something completely different. That fetches your model1 objects from the database and gives them to you in a Collection. That's why when you call delete() on each one, your delete events get triggered.

Understanding the difference between $model0->model1() and $model0->model1 is the key to understanding Laravel relations (personally I didn't even notice the difference for the first year or so of using Laravel, but everything made a lot more sense once I understood that).

Thank you.. It works.. !! made my day.. !! :)

@louischan
Copy link

Btw in 2018 you can replace
Analytic::where('id', '>', 100)->get()->each(function($analytic) { $analytic->delete(); });

with
Analytic::where('id', '>', 100)->get()->each->delete();

For those who wonder how it works, each is one of the "proxies" defined in the Collection class. The __get magic method in Collection class calls the HigherOrderCollectionProxy class which proxies the method call onto the collection items. This feature was added in Laravel 5.4.

@vanderlee
Copy link

Wouldn't it be possible for Builder->delete() to detect if deleting or deleted model event are registered and (optionally) switch to individual deletes?

@arvins-wittymanager
Copy link

Btw in 2018 you can replace
Analytic::where('id', '>', 100)->get()->each(function($analytic) { $analytic->delete(); });

with
Analytic::where('id', '>', 100)->get()->each->delete();

To be more accurate, you can use higher order functions in versions >= 5.4

https://laravel.com/docs/5.4/collections#higher-order-messages

@chrisadipascual
Copy link

On what circumstances will deleting and deleted will fire?

@rico-ocepek
Copy link

@chrisadipascual whenever you call the delete method of a model directly, not when calling the delete method of the query builder

@chrisadipascual
Copy link

chrisadipascual commented Nov 3, 2019

@beasty-web Thanks for the response! Should have tried deleting a single model before asking. I was tinkering with a whereIn query.

The each workaround @arvins-wittymanager described works beautifully!

@rico-ocepek
Copy link

@chrisadipascual he actually quoted me with his response 😄

But be aware that the performance will be significantly worse with the each solution as it essentially does one database query per deleted item

@Iftakharalamrizve
Copy link

@orrd Many Many Thanks For your description .

@nightwalkeryao
Copy link

nightwalkeryao commented Aug 18, 2020

Analytic::where('id', '>', 100)->get()->each->delete();

In case there are thousands of items to delete, the each supposes this will query DELETE FROM table... thousands times, isn't it ?

@orrd
Copy link

orrd commented Aug 18, 2020

@nightwalkeryao Yes, first that will load all of the models into memory, and then call delete on each one (deleting and deleted events will fire), also doing the DELETE from table... query on each one.

@TraLeeee
Copy link

You guys should read this comment on SO. It's really solved the problem.

@orrd
Copy link

orrd commented Dec 24, 2020

You guys should read this comment on SO. It's really solved the problem.

That's a solution for a different problem. For example, the "scenario A" in the original question on this issue would also not trigger that deleting event handler in the SO answer that you linked to. This issue was about someone noticing that certain types of delete code won't trigger delete events on the models.

@kjostling
Copy link

Really old thread but I managed to make this misstake today.
Well, I understand the problem and I guess the only way to implement this is to add triggers in the database when you add soft deletes to a table that relatea to another table with foreign keys and cascade deletes. Not a trivial feat though.

@rs-sliske
Copy link

rs-sliske commented Oct 29, 2021

Really old thread but I managed to make this misstake today. Well, I understand the problem and I guess the only way to implement this is to add triggers in the database when you add soft deletes to a table that relatea to another table with foreign keys and cascade deletes. Not a trivial feat though.

or you can fetch the models and call delete on the model rather than the query builder

replace Class::where(...)->delete() with Class::where(...)->get()->each->delete()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests