Scenario A
Analytic::where('id', 2)->delete();
Scenario B
Analytic::where('id', 2)->first()->delete();
Is this expected behavior?
public static function boot() {
static::deleting(function($obj) {
});
static::deleted(function($obj) {
});
}
referring doc: http://four.laravel.com/docs/eloquent
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.
Is there a workaround?
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();
});
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.
thank u my problem is fix
@orrd Thanks for explanation!
@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).
The Law of Leaky Abstractions
@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
+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.
Btw in 2018 you can replace
Analytic::where('id', '>', 100)->get()->each(function($analytic) {
$analytic->delete();
});
with
Analytic::where('id', '>', 100)->get()->each->delete();
@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.. !! :)
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.
Wouldn't it be possible for Builder->delete()
to detect if deleting
or deleted
model event are registered and (optionally) switch to individual deletes?
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
On what circumstances will deleting
and deleted
will fire?
@chrisadipascual whenever you call the delete
method of a model directly, not when calling the delete
method of the query builder
@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!
@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
@orrd Many Many Thanks For your description .
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 ?
@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.
Most helpful comment
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:
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":
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!):
Here's another way to do the same thing using Laravel's each() function that it gives you for working on Collections...