-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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
[5.6] Disable model relationship touching #23469
Conversation
I get wanting to override $touches in some cases, but explicitly calling ->touch() should never just silently do nothing. It should either throw an exception (TouchedUntouchableModelException or similar) or actually touch. |
Yeah, I also had that feeling. At first I was implementing it only for the relationship touching, but looking at the Rails API, looks like they do both. |
What is this used for? |
When you use the touching feature of Eloquent and you are doing some batch operation dealing with lots of models at the same time (like the incineration feature in Basecamp - deleting lots of data), the touching might generate lots of unnecessary queries, and could potentially slow down the whole process. But, this is not a use case I've encountered myself, I just saw it on Rails and got curious about what it would take to bring it to Eloquent. |
Would like to see something like this go in. I've had to use My use-case is an app that shows updated timestamps to users to they can see how recently activity has occurred. We have background jobs and a moderation team that make changes to these models, but these changes aren't public-facing so we don't want it to appear as though the record has been updated to other users. Being able to wrap it up in a |
Depends on how you want it to behave. Right now, in the
The comment was about the last option, whether it should be a noop, throw an exception, or not disabling at all. I think the Rails API for this feature is as I implemented, by looking at their API and documentation. I'm gonna try to run a small experiment here to confirm that. But let me know what you think. |
@taylorotwell just confirmed the way it's implemented right now matches the Rails API. So if you are ok with it, nothing else needs to be done. Unless you can think of any side effects that I need to take into account. Below you can find how Rails implements it. irb(main):014:0> p = Post.first
Post Load (0.6ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Post id: 1, title: "Lorem", user_id: 1, created_at: "2018-03-12 15:27:45", updated_at: "2018-03-12 15:27:45">
irb(main):015:0> p.title = "Lorem ipsum"
=> "Lorem ipsum"
irb(main):016:0> p.save
(0.2ms) begin transaction
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
SQL (1.0ms) UPDATE "posts" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? [["title", "Lorem ipsum"], ["updated_at", "2018-03-12 15:28:35.939259"], ["id", 1]]
SQL (0.5ms) UPDATE "users" SET "updated_at" = '2018-03-12 15:28:35.946283' WHERE "users"."id" = ? [["id", 1]]
(18.1ms) commit transaction
=> true
irb(main):017:0> User.no_touching do
irb(main):018:1* p.title = "Relationship touching"
irb(main):019:1> p.save
irb(main):020:1> end
(0.2ms) begin transaction
SQL (0.9ms) UPDATE "posts" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? [["title", "Relationship touching"], ["updated_at", "2018-03-12 15:29:09.085786"], ["id", 1]]
(8.6ms) commit transaction
=> true
irb(main):021:0> User.first.touch
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
(0.2ms) begin transaction
SQL (0.7ms) UPDATE "users" SET "updated_at" = '2018-03-12 15:29:19.736664' WHERE "users"."id" = ? [["id", 1]]
(9.7ms) commit transaction
=> true
irb(main):022:0> User.no_touching do
irb(main):023:1* User.first.touch
irb(main):024:1> end
User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> nil |
Actually, I don't think it fits your usecase quite well. You're updating the model itself, which will update the timetamps of the model, for example: >>> $p = Post::create(['title' => 'Lorem', 'user_id' => 1])
"insert into "posts" ("title", "user_id", "updated_at", "created_at") values (?, ?, ?, ?)"
"update "users" set "updated_at" = ? where "users"."id" = ?"
"select * from "users" where "users"."id" = ? limit 1"
>>> Post::withoutTouching(function () use ($p) {
$p->update(['title' => 'Something else']);
});
"update "posts" set "title" = ?, "updated_at" = ? where "id" = ?"
"update "users" set "updated_at" = ? where "users"."id" = ?" As you can see, the model itself got the irb(main):001:0> p = Post.first
Post Load (0.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ? [["LIMIT", 1]]
irb(main):003:0> Post.no_touching do
irb(main):004:1* p.update title: "Lorem"
irb(main):005:1> end
(0.2ms) begin transaction
SQL (0.8ms) UPDATE "posts" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? [["title", "Lorem"], ["updated_at", "2018-03-12 16:00:45.502981"], ["id", 1]]
SQL (0.6ms) UPDATE "users" SET "updated_at" = '2018-03-12 16:00:45.507653' WHERE "users"."id" = ? [["id", 1]]
(8.9ms) commit transaction
=> true But that being said, I think we should not update the model's timestamps in the |
Is the idea to disable all touching for all models? Or just specific models? |
@unstoppablecarl both. You can disable specific models touching or globally. specific: User::withoutTouching(function () use ($post) {
// creates the comment without updating related user from post, but post is touched.
$post->comments()->create(...);
Post::withoutTouching(function () use ($post) {
// creates the comment without touching the post or the user
// related to the post (because of the nested scope).
$post->comments()->create(...);
});
}); globally: use Illuminate\Database\Eloquent\Model;
Model::withoutTouching(function () use ($post) {
// creates the comment without touching any relationship
$post->comments()->create(...);
}); |
@dwightwatson I was wrong. In my implementation, we are not touching the model's timestamp, I had a cached revision here in my testing application. So calls to Example: >>> $p = Post::first()
"select * from "posts" limit 1"
=> App\Post {#761
id: "1",
title: "lorem",
user_id: "1",
created_at: "2018-03-12 15:55:19",
updated_at: "2018-03-12 16:25:34",
}
>>> $p->update(['title' => 'ipsum']); "update "posts" set "title" = ?, "updated_at" = ? where "id" = ?"
"update "users" set "updated_at" = ? where "users"."id" = ?"
"select * from "users" where "users"."id" = ? limit 1"
=> true
>>> Post::withoutTouching(function () use ($p) {
$p->update(['title' => 'lorem']);
});
"update "posts" set "title" = ? where "id" = ?"
"update "users" set "updated_at" = ? where "users"."id" = ?"
=> null But this differs from the Rails implementation. (see the Rails implementation int he comment above #23469 (comment)). Any thoughts on whether we should stick to the Rails API and update the model's |
If it's going to be the same method name as Rails probably it would need to behave the same way so as not to surprise people. |
@taylorotwell Done. Don't think there's anything else to do here. Going to send another PR to cover the usecase @dwightwatson mentioned later today. |
Nitpick, but any particular reason you use |
@afraca good point, I thought I had to use |
Can you call |
I really like this idea. I think this may be a better api for it though.
I have had some cases where I needed to conditionally touch some models in the collection of related models but not others. How would you write that? |
Is that true for every other method in Laravel as well? I'm a little surprised by this opinion. Regarding the general feature: I feel quite strongly that calling |
@JosephSilber I think you are right. I thought of introducing a |
The implementation is based on class ParentModel extends Model {}
class ChildModel extends ParentModel {}
ParentModel::withoutTouching(function () {
// If something triggers updating timestamps of child model relationships,
// it won't touch it because ChildModel instanceof ParentModel === true.
});
I like it. Wish PHP had method overloading. Anyways, so for this API it should not include the class being class? Like, ignore the caller, and use only the given classes?
Not sure about this one. It's doable, but don't think it's that useful, tbh. It would only make it much more complex that it should be, IMO. It should just be "if you encounter relationships of this class, don't touch it".
Can you provide an example? If I got it right, you want to conditionally touch models based on some criteria. Like, if you encounter relationships of class "User" and the ID is 123, don't touch it, if not this ID it's ok to touch it. Can't think of a use case where you are using the relationship touching feature, but you don't want to touch specific models of a |
Gonna make some changes here:
|
I think it's all done here. |
static::$ignoreOnTouch = array_values(array_merge(static::$ignoreOnTouch, $models)); | ||
|
||
try { | ||
call_user_func($callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is call_user_func($callback)
better than $callback()
?
public function shouldTouch() | ||
{ | ||
foreach (static::$ignoreOnTouch as $ignoredClass) { | ||
if ($this instanceof $ignoredClass) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could cause some surprise to users that extend models. I think it would be better to check if(get_class($this) === $ignoredClass)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would disagree, I would be surprised if extending an ignored class causes the child class to not be ignored.
Also, this allows you to have a parent "IgnoredModel" class and extend all your models from that instead of having to explicitly ignore every model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this also allows us to globally ignore relationship touching by invoking it from the base model class. That's the desired behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a getter for $ignoreOnTouch
?
I like that. maybe |
@tonysm where are we at with this? |
@olivernybroe @unstoppablecarl Added the @taylorotwell It's ready |
Just curious, do we have actual use cases for this waiting for this to be merged? Like packages being developed, etc. Or is this mainly something be done "in theory"? 😄 |
All in theory. I haven't seen anyone needing it in the Laravel world. As I said:
Can be closed, no big deal. haha |
We do have a use case. We have been running into a performance problem, where touching of related models is actually a problem! We have hacked ourself around it, but this change would really be appreciated. |
Added
Model::isIgnoringTouch()
method to check if the model is in thewithoutTouching
scopeTL;DR
We can now disable the relationship model touching in a given callback lifetime for specific models when we want to with the new
Model::withoutTouching
andModel::withoutTouchingOn
methods.I saw this functionality in one of DHH's videos (here) and figured I would give it a try to implement in Laravel. Not sure if you find it useful or not, I was mainly interested in what it would take to write it.
ActiveRecord documentation can be found here, for reference.
Examples
Let's say a Post belongs to some User and has the user relationship set in the touches array.
As I'm not that familiar with the Eloquent internals, I'm not entirely sure about the current implementation.