Sorting ActiveRecord models by sub-model attributes? - sql

Lets assume I have a model Category which has_many Items. Now, I'd like to present a table of Categories sorted on various attributes of Items. For example, have the category with the highest priced item at the top. Or sort the categories based on their best rated item. Or sort the categories based on the most recent item (i.e., the category with the most recent item would be first).
class Category < ActiveRecord::Base
has_many :items
# attributes: name
end
class Item < ActiveRecord::Base
belongs_to :category
# attributes: price, rating, date,
end
Which is the best approach?
Maintain additional columns on the Category model to hold the attributes for sorting (i.e., the highest item price or the best rating of an item in that category). I've done this before but it's kinda ugly and requires updating the category model each time an Item changes
Some magic SQL incantation in the order clause?
Something else?
The best I can come up with is this SQL, for producing a list of Category sorted by the max price of the contained Items.
select categories.name, max(items.price) from categories join items group by categories.name
Not sure how this translates into Rails code though. This SQL also doesn't work if I wanted the Categories sorted by the price of the most recent item. I'm really trying to keep this in the database for obvious performance reasons.

Assuming the attributes listed in the items model are database columns there are many things you could do.
The easiest is probably named_scopes
/app/models/category.rb
class Category < ActiveRecord::Base
has_many :items
# attributes: name
named_scope :sorted_by_price, :joins => :items, :group => 'users.id', :order => "items.price DESC"
named_scope :sorted_by_rating, :joins => :items, :group => 'users.id', :order => "items.rating DESC"
named_scope :active, :condition => {:active => true}
end
Then you could just use Category.sorted_by_price to return a list of categories sorted by price, highest to lowest. The advantages of named_scopes lets you chain multiple similar queries. Using the code above, if your Category had a boolean value named active. You could use
Category.active.sorted_by_price to get a list of active categories ordered by their most expensive item.

Isn't that exactly what :joins is for?
Category.find(:all, :joins => :items, :order => 'price')
Category.find(:all, :joins => :items, :order => 'rating')

Related

Rails multiple joins condition query for exact tags with has many through relationships

I have a problem with filtering products by exact tags as my current query does not return exact matches and I can't seem to get the right query conditions.
Example
Area = ["small","big"] ,Surface = ["smooth","rough"]
Product A has only ["small","smooth","rough"] as tags
If I filter products using ["small","big","smooth","rough"] as the tags, I get product A in my search results but ideally, it should not return any search results.
I have three models, Product,Area and Surface. Area & Surface are linked to Product by a has_many through relationship.
class Product < ActiveRecord::Base
has_many :product_areas
has_many :areas, :through => :product_areas
has_many :product_surfaces
has_many :surfaces, :through => :product_surfaces
class Area < ActiveRecord::Base
#Surface model looks exactly the same as Area model
has_many :product_areas,dependent: :destroy
has_many :products, :through => :product_areas
My Query
area_ids = params[:area_ids]
surface_ids = params[:surface_ids]
#products = Product.where(nil)
#products = #products.joins(:areas).where('areas.id' => area_ids).group('products.id').having("count(areas.id) >= ?",area_ids.count) unless area_ids.blank?
#products = #products.joins(:surfaces).where('surfaces.id' => surface_ids).group('products.id').having("count(surfaces.id) >= ?",surface_ids.count) unless surface_ids.blank?
I solved this problem just now with this solution.
First I used the names of the models for Area & Surface as their unique identifer as they can have conflicting ids and added them to an array.
Next I looped through the products and created an array of the name identifers and compared the two arrays to check if they intersect. Intersection would mean that the search filters were a correct match and we add the product ID to a third array which stores all the product_ids before doing a query to get the products with those product ids.
#area = Area.all
area_ids = params[:area_ids]
#uniq_names = #area.where(id: area_ids).collect { |m| m.name }
#products.each do |product|
#names = product.areas.map { |m| m.name }
# if intersect, then we add them to filtered product
if (#uniq_names - #names).empty?
product_ids << product.id
end
end
#products = Product.where(id: product_ids)

ActiveRecord sort children within parent

Is there a way to pre-sort the children of a parent through ActiveRecord (Rails 3.2.13)?
So if you have a setup like this
class Parent < ActiveRecord::Base
has_many :children
[...]
class Children < ActiveRecord::Base
belongs_to :parent
Something like this:
p = Parent.where(:name => 'Diana').includes(:children, :order => 'd_o_b DESC')
That way when I call p.children I am getting an array of objects ordered by birth, and not by their database ID.
Or do I just need to sort my array afterward?
In your Parent model, change the has_many to:
has_many :children, :order => 'd_o_b DESC'
Then anytime you access the children association for a parent record (e.g., #parent.children), they'll be in descending order of date of birth.

Writing a named scope in rails

I have three models: Products, Placements, Collections
I'm trying to write a name scope that only chooses products NOT in a certain collection.
products has_many :collections, :through => :placements
collections has_many :products, :through => :placements
I got about this far:
scope :not_in_front, joins(:collections).where('collections.id IS NOT ?', 4)
But that generated the opposite of what I expected in the query:
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "placements" ON "products"."id" = "placements"."product_id" WHERE "placements"."collection_id" = 4
Any idea how to write this to only select the products not in that particular collection?
Instead of collections.id IS NOT 4 try collections.id != 4
The named scope was getting too ugly, so I went with this. Not sure it's the best way, but, it works...
def self.not_on_top_shelf
top_shelf = Collection.find_by_handle('top-shelf')
products = Product.find(:all, :order => "factor_score DESC")
not_on_top_shelf = products.map {|p| p unless p.collections.include?(top_shelf)}
not_on_top_shelf.compact #some products may not be in a collection
end

Rails: ActiveRecord Query for has_many :through model

How to query for Companies with a certain Branch in a "has_many :through" relationship?
#company.rb
has_many :branch_choices
has_many :branches, :through => :branch_choices
"Find all companies with Branch ID 3"
Company.includes(:branches).where(:branches => {:id => 3})
or
Branch.find(3).companies
UPDATE
Actually, there's one downside to the first snippet: it eagerly loads branches along with the companies. To avoid this overhead you may consider using a left join:
Company.
joins("LEFT JOIN `branch_choices` ON `branch_choices`.`company_id` = `companies`.`id`").
where(:branch_choices => {:branch_id => 3})

Can I :select multiple fields (*, foo) without the extra ones being added to my instances (Instance.foo=>bar)

I'm trying to write a named scope that will order my 'Products' class based on the average 'Review' value. The basic model looks like this
Product < ActiveRecord::Base
has_many :reviews
Review < ActiveRecord::Base
belongs_to :product
# integer value
I've defined the following named scope on Product:
named_scope :best_reviews,
:select => "*, AVG(reviews.value) score",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "score desc"
This seems to be working properly, except that it's adding the 'score' value in the select to my Product instances, which causes problems if I try to save them, and makes comparisons return false (#BestProduct != Product.best_reviews.first, becuase Product.best_reviews.first has score=whatever).
Is there a better way to structure the named_scope? Or a way to make Rails ignore the extra field in the select?
I'm not a Rails developer, but I know SQL allows you to sort by a field that is not in the select-list.
Can you do this:
:select => "*",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "AVG(reviews.value) desc"
Wow, so I should really wait before asking questions. Here's one solution (I'd love to hear if there are better approaches):
I moved the score field into the inner join. That makes it available for ordering but doesn't seem to add it to the instance:
named_scope :best_reviews,
:joins => "INNER JOIN (
SELECT *, AVG(value) score FROM reviews GROUP BY reviews.product_id
) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "reviews.score desc"