Ruby on Rails - maximum count of associated objects? - ruby-on-rails

I need help with a query. I have multiple Canteens, where each has multiple Meals, where each meal has multiple MealPicks.
Although I don't know if this MealPick model is good idea, because I need to display how many times has the meal been picked TODAY, so I needed the timestamp to make this query.
class Meal < ActiveRecord::Base
def todays_picks
meal_picks.where(["created_at >= ? AND created_at < ?", Date.today.beginning_of_day, Date.today.end_of_day])
end
end
Before I had just a meal_picked_count counter in Meal which I incremented by increment_counter method.
Okay so, now I need to display for each Canteen the Meal that has the most MealPicks, I played around in the console and tried something like Canteen.find(1).meals.maximum("meal_picks.count") but that obviously does not work as it is not a column.
Any ideas?

You can do this:
MealPick.joins(:meal => :canteen)
.where("canteens.id = ?", 1)
.order("count_all DESC")
.group(:meal_id)
.count
That will return an ordered hash like this:
{ 200 => 25 }
Where 200 would be the meal id and 25 would be the count.
Update
For anyone interested, I started playing around with this to see if I could use subqueries with ActiveRecord to give me meaningful information than what I came up with before. Here's what I have:
class Meal < ActiveRecord::Base
belongs_to :canteen
has_many :meal_picks
attr_accessible :name, :price
scope :with_grouped_picks, ->() {
query = <<-QUERY
INNER JOIN (#{Arel.sql(MealPick.counted_by_meal.to_sql)}) as top_picks
ON meals.id = top_picks.meal_id
QUERY
joins(query)
}
scope :top_picks, with_grouped_picks.order("top_picks.number_of_picks DESC")
scope :top_pick, top_picks.limit(1)
end
class MealPick < ActiveRecord::Base
belongs_to :meal
attr_accessible :user
scope :counted_by_meal, group(:meal_id).select("meal_id, count(*) as number_of_picks")
scope :top_picks, counted_by_meal.order("number_of_picks DESC")
scope :top_pick, counted_by_meal.order("number_of_picks DESC").limit(1)
end
class Canteen < ActiveRecord::Base
attr_accessible :name
has_many :meals
has_many :meal_picks, through: :meals
def top_picks
#top_picks ||= meals.top_picks
end
def top_pick
#top_pick ||= top_picks.first
end
end
This allows me to do this:
c = Canteen.first
c.top_picks #Returns their meals ordered by the number of picks
c.top_pick #Returns the one with the top number of picks
Let's say that I wanted to order all meals by the number of picks. I could do this:
Meal.includes(:canteen).top_picks #Returns all meals for all canteens ordered by number of picks.
Meal.includes(:canteen).where("canteens.id = ?", some_id).top_picks #Top picks for a particular canteen
Meal.includes(:canteen).where("canteens.location = ?", some_location) #Return top picks for a canteens in a given location
Since we are using joins, grouping, and server-side counts, the whole collection need not be loaded to determine the pick count. This is a bit more flexible and probably more efficient.

canteen.meals.max {|m| m.meal_picked_count}

Related

Caching association that was in where clause

Let me show an example:
I have 2 models:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
scope :created_in, ->(start_date, end_date) { where(created_at: start_date..end_date) }
end
What I want is to get users that created post during a specific period:
users = User.includes(:posts).joins(:posts).merge(Post.created_in(start_date, end_date))
Is it somehow possible to cache posts that are in the where clause? So after I do
users.first.posts
it will show me exactly those posts that match the condition without producing any additional queries.
No, I don't think this is possible. Depending on the context, what you can do is to do a lookup table which you memoize / cache. Something like
User.all.each do |user|
posts = posts_by_user_id[user.id]
end
def posts_by_user_id
#_posts_by_user_id ||= posts.group_by(&:user_id)
end
def posts
Post.created_in(start_date, end_date)
end

How do you sort a collection by distant relationships?

I have a tree-like relationship model with a fixed depth, and each level has a code attribute - similar to this;
class Category < ActiveRecord::Base
has_many :sub_categories
default_scope order(:code)
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_many :items
def self.sorted
self.joins(:category).order('"categories".code ASC, "sub_categories".code')
end
end
class Item < ActiveRecord::Base
belongs_to :sub_category
def self.sorted
# what goes here?
end
end
Category.all gets all the the categories ordered by categories.code.
SubCategory.sorted gets all the sub_categories ordered by categories.code, sub_categories.code. I used this approach because default_scope : joins(:categories).order('categories.code, sub_categories.code') makes .find return read-only records.
I would like to call Items.sorted and get the all items ordered by categories.code, sub_categories.code, items.code but I can't figure out how. I imagine I need a second .joins, but I don't have a relationship name to supply.
Try this:
class Item < ActiveRecord::Base
belongs_to :sub_category
def self.sorted
# do not need self here as that is implied
joins(sub_category: :category).
order('"categories".code ASC, "sub_categories".code, "items".code')
end
end
See the docs for joining nested assoications here
This works, but it seems like there should be a better way;
def self.sorted
joins(:sub_category).
joins('INNER JOIN "categories" on "categories".id = "sub_categories".category_id').
order('"categories".code ASC, "sub_categories".code ASC, "items".number ASC')
end

Rails 3: Retrieve all child records where parent model attribute equals search key

I want to do a query that returns only the assets that do not have a serial number where the workorder branch equals a number.
class Workorder < ActiveRecord::Base
belongs_to :user
has_many :assets
scope :current_branch, where("branch=350").order("wo_date ASC")
end
class Asset < ActiveRecord::Base
belongs_to :workorder
scope :needs_serial, :conditions => {:serial => ""}
end
class AssetsController < ApplicationController
def index
#assets_needing_serial=???
end
end
So I want a hash of :assets where the assets.workorder.branch="350". I think I could do a loop and create the hash that way but should I be able to do this in a query? Should I be trying to use scopes for this?
**Update
This is what I ended up using. Worked great.
#assets = Asset.joins(:workorder).where('workorders.branch=350').order('workorders.wo_date ASC')
The query you would want to do is
Asset.joins(:workorder).where('workorders.branch = 325')
So you can make a scope like this:
scope :with_workorder_branch, lambda { |branch| joins(:workorder).where('workorders.branch = ?', branch) }
If you're going to be looping through the workorders, you should change the joins to includes as this eager loads them.
The rails guide to queries is very helpful for this sort of thing http://guides.rubyonrails.org/active_record_querying.html

Rails: Getting random product within several categories

I have a question about random entries in Rails 3.
I have two models:
class Product < ActiveRecord::Base
belongs_to :category
self.random
Product.find :first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i
end
end
class Category < ActiveRecord::Base
has_many :products
end
I'm able to get a random product within all products using an random offset castet to int. But I want also be able to get random products WITHIN several given categories. I tried something like this, but this doesn't work, because of the offset index:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).where(:first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i)
end
end
Anybody here who knows a better solution?
thx!
tux
You could try and simplify this a little:
class Product < ActiveRecord::Base
def self.random(cat = nil)
cat ||= %w[ Mac Windows ]
joins(:categories).where(:categories => { :name => cat }).offset(ActiveSupport::SecureRandom.random_number(self.count)).first
end
end
Rails 3 has a lot of convenient helper methods like offset and first that can reduce the number of arguments you need to pass to the where clause.
If you're having issues with the join, where the number of products that match is smaller than the number of products in total, you need to use ORDER BY RAND() instead. It's actually not that huge a deal performance wise in most cases and you can always benchmark to make sure it works for you.
the offset happens after the order, so you can add .order('rand()') then you will be getting random elements from multiple categories. but since the order is random you also do not need offset anymore. so just:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).order('rand()').first
end
end

ActiveRecord query ordering

Suppose I have the following models:
class Car < ActiveRecord::Base
belongs_to :seat
...
end
class Seat < ActiveRecord::Base
belongs_to :color
...
end
class Color < ActiveRecord::Base
attr_reader :name
...
end
If I have get a list of Cars, and I want to order the Cars by color.name, how to write the order query?
class Car < ActiveRecord::Base
belongs_to :seat
...
def cars_order_by_color(car_ids)
where(:id=>car_ids).order(?????) #HOW TO ORDER BY COLOR.name
end
end
If you use a joins on your query, you can then sort by the joined tables (either seats or colors):
Car.joins(:seat => :color).order("colors.name")
To retrieve records from the database in a specific order, you can specify the :order option to the find call.
Car.order("color")
You could specify ASC or DESC as well:
Car.order("color DESC")
For more help in query look here: active_record_querying
Hope this helps.
Edit
You can use find_by_sql:
Car.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc")
Write appropriate query.

Resources