Skip to content

[9.x] Add orderBy property for default model sorting#40678

Closed
jasonmccreary wants to merge 1 commit intolaravel:9.xfrom
jasonmccreary:model-orderby-property
Closed

[9.x] Add orderBy property for default model sorting#40678
jasonmccreary wants to merge 1 commit intolaravel:9.xfrom
jasonmccreary:model-orderby-property

Conversation

@jasonmccreary
Copy link
Copy Markdown
Contributor

@jasonmccreary jasonmccreary commented Jan 27, 2022

This adds an optional orderBy property to Eloquent models which allows the developer to easily add default sorting to any model.

Why
I believe this is a common enough use case where having something built-in adds to that "comes with everything right out-of-the-box" feel Laravel has.

While this may be done currently, it's a rather cumbersome experience: overwriting the boot method, registering a global scope, writing your own logic.

How
Instead, you may simply set the orderBy property with the column (or columns) and direction (optional) you want your Eloquent queries to be ordered by. For example:

class Post extends Model
{
    public $orderBy = [
        'published_at' => 'DESC',
        'title',
    ];
}

Note: To avoid this feature being a "foot gun" with the complexity of merging default and runtime orders, this is only applied when orderBy was not called in the Eloquent query. Calling orderBy in an Eloquent query assumes the developer wants full control over the sorting.

@GrahamCampbell GrahamCampbell changed the title Add orderBy property for default model sorting [9.x] Add orderBy property for default model sorting Jan 27, 2022
@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@jasonmccreary Great stuff. My only reservation is whether an 'always-on' Concern trait like this might be needlessly filling up the global-scopes namespace with global scope names that get applied to every model.

What if this was an opt-in trait, like SoftDeletes?

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

jasonmccreary commented Jan 28, 2022

@jonnott, sure. could go that way. The DX of simply setting a property better keeps with the underlying goal - less code. Once you start adding traits, setting properties, defining methods it quickly becomes the same amount of code for the user to write.

In fairness, nothing is "needlessly filled". I took care to not add anything if the model does not set the $orderBy property. So either solution would apply the same amount of scopes/macros.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

...i.e.

namespace Illuminate\Database\Eloquent;

use DefaultOrderBy;

class User extends Model {

protected $orderBy = [ .. ];

}

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

In fairness, nothing is "needlessly filled". I took care to not add anything if the model does not set the $orderBy property. So either solution would apply the same amount of scopes/macros.

Oh yes, if (empty($this->orderBy)) { return; } is a nice touch.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@jonnott, sure. could go that way. The DX of simply setting a property better keeps with the underlying goal - less code. Once you start adding traits, setting properties, defining methods it quickly becomes the same amount of code for the user to write.

I think because none of the other Concerns/Has* traits define named global scopes, it feels a bit like by doing so this trait is infringing on something which feels like the 'domain' of the app's developer - i.e. the global-scope-names-namespace. This is in contrast to say the SoftDeletes trait, which uses a global scope class instead. What if the 'defaultOrderBy' scope was defined in a class? Am I being too pedantic?

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

..and where would it live .. Database/Eloquent/Concerns/Scopes/DefaultOrderBy.php ?

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@jonnott, all fair points. Let's see what the Laravel team has to say. Glad to make tweaks as I feel this is a worthy addition. But I'd like to make sure they think so too first. 😅

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@jonnott, all fair points. Let's see what the Laravel team has to say. Glad to make tweaks as I feel this is a worthy addition. But I'd like to make sure they think so too first. 😅

Fully agree about the worthiness of the addition. I'd use it loads!

But also wanting to be wary of the possibility of:

  • Models having too many Concerns by default - is that a slippery slope?
  • potential developer pain if trying name their own global scopes something which the default Eloquent Model has already 'gazumped' ;) .. if the core framework utilises a global scope name that a developer may have already used in their code .. is that a breaking change?!

Getting towards an almost philosophical level here, I'm aware 🤓 Looks like one for @taylorotwell et al to review the exact implementation detail for maybe..

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

.. is that a breaking change?!

@jonnott, indeed, that's why it's targeted for Laravel 9. 👍

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@jonnott, indeed, that's why it's targeted for Laravel 9. 👍

I'd be interested to know if the core framework actually uses any named global scopes itself, or if the implementation of all framework-level global scoping is done through classes & FQCNs. I'd not be surprised if this is the case.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@jasonmccreary I branched and added a commit to put the global scope into a class instead .. see what you think @ 9.x...jonnott:model-orderby-property

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@jasonmccreary, yeah, more closely follows the SoftDeletes. The only reason I didn't do this was the scope is now aware of the property. That felt strange to me. But weighed against the "magic string", it is better.

I also thought about making the scope class generic, so it could be used directly. But then realized there isn't really a strong use case for that. Again, the streamlined DX is ultimately what makes this worthwhile.

@taylorotwell
Copy link
Copy Markdown
Member

taylorotwell commented Jan 28, 2022

One concern I might have with this being implemented as a global scope is there are certain times in (maybe in the framework, maybe in packages, etc.) where we explicitly retrieve models with all global scopes disabled in order to be sure we can definitely retrieve them without any soft delete constraints getting in the way, etc. In those cases, this ordering would be removed - which I find... odd... because you may want the sorting to still be in affect even though you don't want any other global scopes in play.

I think the reason it starts to feel odd is this is not a "scope" at all, where I typically think of "scope" to mean something that "constrains" a query's results into some smaller result set via some where clause condition. This is a pure sorting operation.

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@taylorotwell, agree. This was a bit of a proof of concept. If you feel this is a worthwhile addition, an implementation directly within Eloquent or the Builder may avoid the oddities you mentioned.

@taylorotwell
Copy link
Copy Markdown
Member

I think there is something worthwhile here in general if we nail out how it is implemented. I've definitely had situations where models are very chronological (think blog posts, comments, etc.) and I almost always want them in a given order.

@taylorotwell taylorotwell marked this pull request as draft January 28, 2022 19:37
@jasonmccreary jasonmccreary changed the title [9.x] Add orderBy property for default model sorting [9.x] WIP: Add orderBy property for default model sorting Jan 28, 2022
@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@taylorotwell, cool. I also marked this was WIP. Let me see if I can ingrain this a bit more within Eloquent/Builder so it has less reaching implications you and @jonnott have noted.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Jan 28, 2022

@taylorotwell, cool. I also marked this was WIP. Let me see if I can ingrain this a bit more within Eloquent/Builder so it has less reaching implications you and @jonnott have noted.

I think maybe this functionality could just be worked right into where the query builder applies other normal ordering from ->orderBy()s to the query. Without having looked at that code in detail, not entirely sure how that would happen or if it's trivial to do. I'd imagine that's more like what @taylorotwell is getting at though.

The more I think about it, the more what Taylor's saying makes sense. Using global scopes to do things like this doesn't feel quite right, unless they're opt-in traits like SoftDeletes.

@jasonmccreary jasonmccreary force-pushed the model-orderby-property branch 2 times, most recently from 0798bcc to d45ca03 Compare February 1, 2022 19:13
@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@taylorotwell, this has been updated to push everything down to the Eloquent Builder. The sorting from this property is applied before the query is executed.

However, given the infinite ways developers may write Eloquent queries and the interaction with the underlying Database Query Builder, I am not sure if this is applied everywhere it should be.

As noted in the updated PR description, I only apply this sorting if no other ordering has been set for the query. I believe this helps avoid situations where queries with explicit calls to orderBy sort unexpectedly because it's being merged with the sorting from this new property.

Put simply, this property allows the user to set a default sorting when not otherwise specified.

@jasonmccreary jasonmccreary force-pushed the model-orderby-property branch from d45ca03 to 0ac7b77 Compare February 1, 2022 19:24
@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 1, 2022

@jasonmccreary I still think calling this whole thing 'defaultOrderBy' rather than 'orderBy' might be less confusing for developers..

If it's just called 'orderBy' then people might think that's how you always define ordering for queries, rather than the $query->orderBy() method?

@driesvints
Copy link
Copy Markdown
Member

Remember that draft PR's aren't reviewed. Please mark as ready if you want a new review.

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@driesvints, sorry, I had sent this to Taylor as I worried it might need more work. I have marked it for review so we can make those tweaks and hopefully get it into L9. 🤞

@jasonmccreary jasonmccreary marked this pull request as ready for review February 4, 2022 11:47
@jasonmccreary jasonmccreary changed the title [9.x] WIP: Add orderBy property for default model sorting [9.x] Add orderBy property for default model sorting Feb 4, 2022
@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@driesvints, sorry, I had sent this to Taylor as I worried it might need more work. I have marked it for review so we can make those tweaks and hopefully get it into L9. 🤞

@jasonmccreary @taylorotwell Still I think the main issue with the re-done implementation is that there really needs to be a way to remove the default ordering for those edge-cases where you really need the resulting query (SQL) to have no ORDER BY clause at all (i.e. natural table order) even though you've set this property on the model. Examples might be within tests, for instance.

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

jasonmccreary commented Feb 5, 2022

@jonnott, you should be able to call reorder (with no arguments) just as you would currently to remove all existing order by clauses.

If not, I can add some tests cases to ensure that works.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@jonnott, you should be able to call reorder just as you would currently.

Ah, ok. Looking at your code, I couldn't make out how calling reorder() would remove the default ordering too, as it only clears what's in the ->orders property on the builder, rather than removing the ->orderBy property of the model, which gets handled later in the query building process. I know you had the withoutDefaultOrder() method to do this, but then removed it.

Can you clarify if/how the default ordering would also get removed by ->reorder() ?

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@jonnott, I'm not sure it does, tbh.

Thinking about it more, I don't see removing all orderBy including the default as a common use case if you were defaulting the order for a model. At least not without explicitly setting the desired order - which does overwrite the default.

That could be a breaking change. Which is why this is targeted for 9.0. But even so, you have to opt-in.

In your case, I would change your test cases to have an explicit order. Not try to remove all ordering.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@jonnott, I'm not sure it does, tbh.

I think what I'm really getting at here is consistency with how the rest of the query builder and Eloquent work.

Essentially, anything else that you add into a query, even things added to a model 'as default' (like global scopes) can be removed .. e.g. ORDER BYs with reorder(), global scopes with withoutGlobalScope(), etc .. therefore whatever things you've configured your model with, you can always, by calling the required methods, get back to a completely 'plain' SELECT query, i.e. SELECT * FROM {table}. Period. *** Nothing takes away your ability to get back to that basic query.

..apart from this new orderBy property. How it is now in this PR, once you've defined that as a property of your model class, there no way to get back to that pure SELECT * FROM {table} anymore. Whatever your reason for needing to do so is.. And someone, somewhere, will for sure arrive at a situation where they need to do that. That's why I feel not having that 'way out' isn't very Laravel-y, or consistent within Builder/Eloquent.

*** Correct me if I'm wrong on that one!

Really not trying to just pick holes here, genuinely want this to be a killer Eloquent feature.

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@jonnott, at this point, it's pretty unlikely this will make it into the L9 release being 3 days away. Especially with all our back and forth.

I appreciate the feedback. It's likely there's more work to do here. But I'd really like to let the Laravel Team weigh in first to help guide us in the right direction.

If you have your own ideas of how this should work, I encourage you to use what I have here to open your own PR. 👍

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

If you have your own ideas of how this should work, I encourage you to use what I have here to open your own PR. 👍

I think what you've (re)-done is great. My suggested changes would be really simple, and essentially the same as what you already had in place before, but removed..

  • I'd name the model property $defaultOrder so it doesn't get confused with the builder ->orderBy() method, and call the applying method in Eloquent/Builder applyDefaultOrder() to match.

  • Add a protected boolean $withoutDefaultOrder = false property to Eloquent/Builder, and a method withoutDefaultOrder() to Eloquent/Builder which sets $withoutDefaultOrder to true, and return early from applyDefaultOrder() if either empty($this->orders) or $this->withoutDefaultOrder are true.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@jasonmccreary I think that what you've done so far, combined with my suggestions/changes, ..

  • fully round this out as a neat & complete much-needed new feature
  • provides clear, unambiguous naming & clarity for devs as to what it does
  • make it a no-brainer to merge
  • causes zero BC breaks
  • could totally be targeted at both Laravel 8.x and 9.x without any issue

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@jasonmccreary Seems like $defaultOrderBy / applyDefaultOrderBy() / $withoutDefaultOrderBy / withoutDefaultOrderBy() work equally well regards naming, and fits with how you've named the tests.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

@jasonmccreary I've created a new commit with my suggested changes, which you could merge if you'd like .. jonnott@93e3ad3

I went for $useDefaultOrderBy as the Eloquent/Builder boolean property name in the end, so it doesn't have the same name as either of the new methods added to Eloquent/Builder (applyDefaultOrderBy() and withoutDefaultOrderBy()), which could make the naming confusing.

I guess the only thing needed further to this might be an additional test for withoutDefaultOrderBy() perhaps?

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 5, 2022

..and here's that test jonnott@c59f20c (same branch again on my fork)

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 7, 2022

@jasonmccreary Any thoughts?

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@jonnott, nope. Waiting to see what the Laravel Team thinks.

public function get($columns = ['*'])
{
$builder = $this->applyScopes();
$builder = $this->applyOrderBy()->applyScopes();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's if order by is applied via scope, and based on the code scope only applied after order by?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crynobone That's a very good point. Maybe @jasonmccreary these chained calls need to be the other way around?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crynobone @jasonmccreary Fix for the method ordering committed to my fork jonnott@b835133

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonmccreary I've also added a test for @crynobone's mentioned situation where an orderBy() is applied via a scope: jonnott@231d4f6 (test would've failed without the switching of applyDefaultOrderBy()->applyScopes() to applyScopes()->applyDefaultOrderBy())

@driesvints
Copy link
Copy Markdown
Member

Wait for Taylor to confirm this but I think it's best that we retarget to 10.x since we're releasing today. It's a bit tricky to still merge this one in.

@jonnott
Copy link
Copy Markdown
Contributor

jonnott commented Feb 8, 2022

Wait for Taylor to confirm this but I think it's best that we retarget to 10.x since we're releasing today. It's a bit tricky to still merge this one in.

100% agree this still isn't quite ready for today's 9.0 release. Even this morning it's needed further fixes and tests adding, thanks to @crynobone's helpful observation.

@jasonmccreary
Copy link
Copy Markdown
Contributor Author

@driesvints, no worries. Things got a bit noisy here and I didn't really know which final tweaks this needed. This does work, but agree there are a lot of boundary cases to consider.

I'll close this for now. I'm hopeful we won't need to wait a year until 10.x to get this added. As a new property it would be opt-in. And to the point above, it shouldn't have any breaking changes once the right tweaks are made.

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

Successfully merging this pull request may close these issues.

5 participants