-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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
[11.x] Eloquent inverse relations #51582
[11.x] Eloquent inverse relations #51582
Conversation
Thanks for submitting a PR! Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface. Pull requests that are abandoned in draft may be closed due to inactivity. |
This would be great to have in the core, ever since Jonathan Reinink wrote a great blog post about optimizing circular relationships in Laravel this has been on my mind. The package stancl/laravel-hasmanywithinverse ran with that idea and implemented it for the The one downside is that this can cause an infinite loop in |
I haven't done any mitigation for it yet, but I have encountered it before, so it's on my radar. Partly I'm putting this up in draft right now so that I can start getting feedback before I sink much more time into it (if it's likely to get rejected, it'll probably be because it started getting complex before people could assess what it's actually about). |
Why is it called "inverse" instead of something more sensible like |
Essentially it's marking the reverse/inverse relationship, not just a model that it's linked to. They're already referred to as "inverse" relations in the documentation. |
Wouldn't this causes an infinite loop in |
Potentially, yes. Well... no, this wouldn't cause it - the potential already exists when you use |
I feel like it's clunky you have to pass the name of the inverse relationship which almost always matches the model you are in at the time. |
I've pushed up changes to make |
2cf444f
to
eb42cbf
Compare
3e7ae71
to
58a9f01
Compare
58a9f01
to
cc25c9d
Compare
I disagree; the word "inverse" in the docs is used as an indication that |
No, it's setting the "parent" model as the inverse relationship. That's why this isn't on any
I could do |
Yes you are right, the code indeed sets the inverse relationship to the "parent" exactly as you say, but the word This PR is a good idea anyway regardless of which word is used :) |
src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsInverseRelations.php
Outdated
Show resolved
Hide resolved
ff6ca30
to
bea61e6
Compare
Love the idea
It would and maybe the |
@samlev what's the status of this PR? Might be best to resend it another time when you have time to finish it? |
@driesvints it had been ready to go for a while, except for a concern about I just moved this PR back to draft this week because it had been sitting open for a while with no action, so I assumed that the |
I don't think you need to move it to draft if Taylor hasn't. He'll review your PR once he has the chance 👍 |
@samlev can you elaborate on what would cause the infinite loop issue you reference? Maybe provide a set of example models and relations that would cause that? |
@taylorotwell the basic issue is if you have circular references in relationships, then A toy example: $user->posts->each->setRelation('author', $user);
$user->toArray(); // causes infinite recursion on $user->posts[0]->author->posts[0]->author->posts[0]... This is already an issue in the existing code for models and I have a half-complete PR to resolve it, but it's not something that I think is appropriate for this PR. I'm happy to put this PR on hold until I get the other ready. |
Found some time, eventually. The recursive/circular relations issue should be resolved/resolvable with #52461 |
Thanks! |
@samlev Hey, thanks for the feature. What was the reason to keep |
@christophrumpel The alias was added by @taylorotwell here: 98400bf |
Ah I see, so I guess "chaperone" is now the official name? 😅 thx @PerryvanderMeer |
That's a question for @taylorotwell - But |
great addition @samlev |
Great addition @samlev Should this be expanded for belongsTo relationships? On the same example of the mentioned blog public function show(Category $category, Product $product)
{
$product->setRelation('category', $category);
} |
@moisish The problem with Maybe you could inject it into
class Post extends Model
{
public function comments(): HasMany
{
// Each comment has exactly one post
// so we can set this post as the post on
// each comment.
return $this->hasMany(Comment::class)->chaperone();
}
public function user(): BelongsTo
{
// The user has many posts, but we only have
// this one post - if we set the inverse relationship
// on the user, then it would be missing every other
// post that the user has.
return $this->belongsTo(User::class)->chaperone();
}
} In your example the category has many products, and that's the relationship where you would add class Category extends Model
{
public function products(): HasMany
{
return $this->hasMany(Product::class)->chaperone();
}
} |
How many times has this happened to you?
With the policy:
Only to find that you're now getting a new query for every single comment to pull the post back out of the database?
Obviously you could eager-load the post back onto the model, but even then that's creating a lot of extra busywork and it might miss scopes that were added on the original query:
Well it's happened to me. A lot. So I decided to do something about it.
Introducing
inverse()
inverse()
is a modifier forHas*
andMorph*
relations that allows you to easily set the parent model in the correct relation on the child models:Now you can do:
And each
Comment
model will have itspost
relation filled out with the actual instance of the parent model, with no extra queries, or missed scopes.Note that it injects the actual instance, instead of querying and pulling another instance which could then get out of sync:
Optionally Skipping an Inverse Relationship
If you don't want to incluide the inverse relationship for some reason, you can tack
->withoutInverse()
onto the query builder that you get from the relation:Guessing the Inverse Relationship
If you don't define the inverse relationship it will attempt to guess the correct relationship based on the foreign key of the relation, the foriegn key of the parent, the name of the parent model, the name of a polymorphic relationship, or a couple of common relationship names such as
owner
.If the parent is the same class as the child, it will also guess
parent
.Backwards Compatibility
There aren't really any backwards compatibility concerns - the new method is "opt-in", and the changes made to the existing relationships shouldn't have any affect if the new method isn't used.
Which Relationships Can Be Inverted?
Only the
HasOneOrMany
relationships make sense forinverse()
. This means that it's available forHasOne
,HasMany
,MorphOne
, andMorphMany
. It also supports theOneOfMany
modifiers.None of the
BelongsTo*
relations really make sense to define an inverse relationship because the inverse will probably be a collection.Potential risks
As mentioned by @stayallive, there is a known issue with
getQueueableRelations()
andrelationsToArray()
when you have circular object references, leading to an infinite loop as it tries to dig down through the "tree" to discover relations. While this change isn't introducing that issue (it already exists), it does increase the likelihood that developers will encounter it.I don't think that trying to fix it in this PR is wise, because that will increase the complexity of this change and, again, this change doesn't create the issue - it just makes it more likely for developers to encounter it. I've personally encountered it in my own work when I was using
setRelation
to manually do whatinverse()
does.I do have a mitigation in mind for it, but I think that I'll reserve that for a separate PR. In short, it could be mitigated by keeping track of the objects that have been serialized, and don't keep digging down on ones that we have already seen - it shouldn't be particularly more memory hungry (it will involve temporarily holding another array of references to objects), but it does need to get added in a number of places.