Replies: 1 comment
-
Just wanted to weigh in that I've been working with some mentoring clients lately on custom relationships and it DOES seem that |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Context
I would like to propose an overhaul of the underlying implementation of the Laravel
Relation
classes (HasMany
,BelongsTo
, etc). To understand the reasoning, it's important to understand how relationships are queried in Laravel.Querying Relationships (as it exists today)
Right now, Laravel queries relationships three different ways depending on whether you're loading a relationship directly, loading it eagerly using
with()
, or querying its existence using something likehas()
.Expand summary of existing implementation
Non-Eager Mode
In the simplest terms, this is may look something like:
Relation
object is instantiated andaddConstraints()
is called. The object needs to check whetherRelation::$constraints
has been set to determine if we're loading the relation or using the object for eager loading or an aggregate function. If the flag is true, the relation should add any necessary constraints to the query builder.getResults()
is called on the relation to get the results, which are then passed toModel::setRelation
to be applied to the model.Eager Mode
In the simplest terms, this is may look something like:
Relation::$constraints
flag is set to false, and then aRelation
object is instantiated andaddConstraints()
is called. BecauseRelation::$constraints
has been set to false,addConstraints
is expected to do nothing.addEagerConstraints
is called, which needs to set up the query builder for eager loading.getEager()
is called on the relation to get the eager results, which are then passed tomatch()
so that the results can be matched back to the parents.Aggregate Mode
This is when you're calling something like
whereHas()
that's not actually returning the relation models, but checking existence or counting them. It may look something like:Relation
object is instantiated withRelation::$constraints
set to false so that the default constraints are not applied (this is done via agetRelationWithoutConstraints()
method on the Eloquent builder).getRelationExistenceQuery
orgetRelationExistenceCountQuery
is called. This is a performance optimization, because checking existence can be faster than getting the actual count.getRelationExistence*
call is then added to the parent query as a subquery and constraints are merged into the parent query.Inserting & Updating Related Models
There are lots of helper functions like
save()
andcreate()
that are responsible for setting the correct keys on the correct models while saving them. These methods are outside of the scope of this suggested change.The Idea
Once you wrap your head around Laravel relationships, there are two things that I believe stand out:
Relation::$constraints
can be confusing and I imagine is the result of backwards-compatible changes over time.In the end, I think querying a relationship can be thought of in the following way:
n
number of parent models, apply constraints to the query builderIn a non-eager context, a naive relation could be implemented as:
Whereas a more sophisticated relation could add some logic branching that looked a little bit like:
By thinking in "eager mode" by default, we can simplify the
Relation
interface, and essentially make "non-eager mode" a performance optimization.The same can be done for aggregate mode. We can use "count" queries by default, and allow relations to define an alternate "existence" query as a performance optimization if needed.
In the end, the relevant parts of the interface would look like:
Realistically, it would have to be a little more complex than this, but I'm trying to stick to the bare minimum for the purpose of example. One thing I'm leaving out for simplicity's sake is the difference between "to one" and "to many" relationships, which complicates things a little but doesn't change the underlying concepts.
Backwards Compatibility
In the end, I think the
Relation
classes could be rewritten without causing any backwards compatibility issues for folks who use the methods defined inHasRelationships
like$this->hasMany()
/etc. The interface for instantiating a new relationship would not change at all.This would be a major breaking change for anyone who has implemented a custom relationship using the existing abstract
Relation
class. As such, it would have to be introduced in the 9.x release at the earliest.(I also think that a separate class could be provided, either within the framework or in a compatibility package, that would implement the new interface using the old
Relation
class methods, so that the initial upgrade path could be to replaceextends Relation
withextends BackwardsCompatibleRelation
.)Summary
In the end, I think this could improve the existing code base and provide a much easier starting point for anyone who wants to implement custom relationships in their own projects.
I already have some work done on this idea, but I don't want to undergo a real pull request against the framework without some buy-in from maintainers (obviously the devil is in the details so I don't expect buy-in here to guarantee a merge in the framework—I just don't want to submit a PR that will never even get considered).
Let me know what y'all think!
Beta Was this translation helpful? Give feedback.
All reactions