Home > Software engineering >  How to sort a model using .order instead of .sort_by?
How to sort a model using .order instead of .sort_by?

Time:10-22

Product has many product attributes:

class Product < ApplicationRecord
  has_many :product_attributes
end

class ProductAttribute < ApplicationRecord
  belongs_to :product
end

I can sort it with sort_by:

@products.includes(:product_attributes).to_a.sort_by do |product|
  product.product_attributes.find_by(title: "Volume").value.to_i
end

Is it possible to make the same sort with order method?
I don’t understand how to order by particular attribute title (like "Volume", etc).

@products = Product.includes(:product_attributes).order( ??? )

Here is similar question:
Rails - Sort by join table data
Maybe I don't see the obvious, but I think it doesn't answer my question. I select item not by attribute name, but by attribute value, like "Volume". In other words, I find_by by attribute's value with title "Volume" (look at the code above). And I don't understand how to make such selection with order.

CodePudding user response:

Try with following

class Product < ApplicationRecord
  has_many :product_attributes
end

class ProductAttribute < ApplicationRecord
  belongs_to :product
end

Product.includes(:product_attributes)
    .where(product_attributes: {title: "Volume" })  
    .order('product_attributes.title ASC')

Reference for Order by the associations two levels deep.

Rails order by association field

Reference for Rails, querying associated records with conditions

Rails, querying associated records with conditions

You can also use Scope With Argument like below

class Product < ApplicationRecord
  has_many :product_attributes

  scope :sort_by_product_attribute_title, ->(title){
    includes(:product_attributes)
        .where(product_attributes: {title: title }) 
        .order('product_attributes.title ASC')  }  
end

class ProductAttribute < ApplicationRecord
  belongs_to :product
end

Product.sort_by_product_attribute_title("Volume")

CodePudding user response:

There are a few different possible solutions - one them is to do a custom join with an additional restiction in the ON clause:

class Product < ApplicationRecord

  has_many :product_attributes

  def self.order_by_volume
    joins(join_on_product_attribute('Volume'))
      .order("volumes.value desc")
  end

  private 

  def self.join_on_product_attribute(title, join_alias = title.downcase.pluralize)
    products, attrs = Product.arel_table, ProductAttribute.arel_table.alias(join_alias)
    products.join(attrs, Arel::Nodes::OuterJoin)
               .on(
                 products[:id].eq(attrs[:product_id])
                   .and(attrs[:title].eq(title))
               )
               .join_sources.first
  end
end
irb(main):084:0> Product.order_by_volume
  Product Load (0.5ms)  SELECT "products".* FROM "products" LEFT OUTER JOIN "product_attributes" "volumes" ON "products"."id" = "volumes"."product_id" AND "volumes"."title" = 'Volume' ORDER BY volumes.value desc

Other possible solutions are a subquery or a lateral join to select the rows off the product_attributes table that just correspond to the volume attribute.

  • Related