39

I've got a RoR project in the works. Here are the applicable sections of my models.

Home

has_many :communities, :through => :availabilities
has_many :availabilities, :order => "price ASC"

Community

has_many :homes, :through => :availabilities
has_many :availabilities

Availability

belongs_to :home
belongs_to :community

The "availabilities" table in the database has the additional data column "price"

So now I can call

@home.availabilities.each do |a|
  a.community.name
  a.price

and get back the availabilities data ordered by price as I want. My question is this:

Is there a way to automatically order Homes by avaliabilities.first.price (first = lowest)? Maybe something with default_scope :order?

6 Answers 6

44

I would suggest to avoid using default_scope, especially on something like price on another table. Every time you'll use that table, join and ordering will take place, possibly giving strange results in complex queries and anyway making your query slower.

There's nothing wrong with a scope of its own, it's simpler and it's even clearer, you can make it as simple as:

scope :ordered, -> { includes(:availabilities).order('availabilities.price') }

I would call it price_ordered.

PS: Remember to add an index on price; Also see other great answers in here to decide between join/include.

3
  • 1
    Do you need to use includes instead of joins?
    – Jwan622
    Nov 1, 2016 at 16:28
  • 1
    Depends, @TeWu is doing a great job at explaining the difference in his answer. In particular the link to Tom Dallimore's blog is awesome.
    – ecoologic
    Nov 2, 2016 at 23:28
  • 1
    @ecoologic stackoverflow.com/questions/56669484/… Jun 20, 2019 at 11:40
23

Figured it out with help from this related post.

I moved the ordering out of the Home model and into the Availability model:

Availability

default_scope :order => "price ASC"

Then I eager loaded availabilities into the Home model and sorted by price:

Home

default_scope :include => :availabilities, :order => "availabilities.price ASC"
16

@ecoologic answer:

scope :ordered, -> { includes(:availabilities).order('availabilities.price') }

is great, but it should be mentioned that includes could, and in some cases should be replaced by joins. They both have their optimal use cases (see: #1, #2).

From practical standpoint there are two main differences:

  1. includes loads associated record(s); in this case Availability records. joins don't load any associated record(s). So you should use includes when you want to use data from join model e.g. display price somewhere. On the other hand, joins should be used if you intend to use join model's data only in query e.g. in ORDER BY or WHERE clauses.

  2. includes loads all records, while joins loads only those records that have associated join model. So in OP's case, Home.includes(:availabilities) would load all homes, while Home.joins(:availabilities) would load only those homes that have associated at least one availability.

Also see this question.

2
  • That optimal use cases link is now broken
    – Masroor
    Aug 10, 2021 at 10:30
  • Thanks for info. The link is broken because the linked blog is dead ... what a shame. I've linked to archived version of this blog post, and also added a link to some not prehistoric blog post on this subject ;)
    – TeWu
    Aug 24, 2021 at 18:56
11

Another way to achieve this:

scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price]) }

You can also specify ASC direction with

scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price].asc) }

DESC:

scope :ordered, -> { includes(:availabilities).order(Availability.arel_table[:price].desc) }

Using arel_table on ActiveRecord model makes you save against scenario when table name changed (but it happens very rarely).

Note that it is nice to add main_table#id for determinate sorting.

So final version would be:

scope :ordered, -> {
  includes(:availabilities).
    order(Availability.arel_table[:price].asc, order(Home.arel_table[:id].asc)
}
7

In Rails 5.2+, you might get a deprecation warning when passing a string param to order method:

DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "table.column". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes.

To solve this, you can use Arel.sql():

scope :ordered, -> {
  includes(:availabilities).order(Arel.sql('availabilities.price'))
}
5

You can also sort linked tables like this (e.g.):

class User
  has_many :posts
end

class Post
  belongs_to :user

  scope :sorted_by_user_and_title, -> { 
    joins(:user).merge(
     User.order(first_name: :asc, last_name: :asc)
    )
    .order(title: :desc)
    # SELECT * FROM `posts`
    # INNER JOIN `users` ON `posts`.`user_id` = `users`.`id`
    # ORDER BY
    # `users`.`first_name` ASC, `users`.`last_name` ASC, `posts`.`title` DESC;
  }
  scope :sorted_by_title_and_user, -> { 
    order(title: :desc)
    .joins(:user).merge(
     User.order(first_name: :asc, last_name: :asc)
    )
    # SELECT * FROM `posts`
    # INNER JOIN `users` ON `posts`.`user_id` = `users`.`id`
    # ORDER BY
    # `posts`.`title` DESC, `users`.`first_name` ASC, `users`.`last_name` ASC;
  }
end

Regards

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.