Rails: Getting random product within several categories - ruby-on-rails

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

Related

How to find records, whose has_many through objects include all objects of some list?

I got a typical tag and whatever-object relation: say
class Tag < ActiveRecord::Base
attr_accessible :name
has_many :tagazations
has_many :projects, :through => :tagazations
end
class Tagazation < ActiveRecord::Base
belongs_to :project
belongs_to :tag
validates :tag_id, :uniqueness => { :scope => :project_id }
end
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
end
nothing special here: each project is tagged by one or multiple tags.
The app has a feature of search: you can select the certain tags and my app should show you all projects which tagged with ALL mentioned tags. So I got an array of the necessary tag_ids and then got stuck with such easy problem
To do this in one query you'd want to take advantage of the common double not exists SQL query, which essentially does find X for all Y.
In your instance, you might do:
class Project < ActiveRecord::Base
def with_tags(tag_ids)
where("NOT EXISTS (SELECT * FROM tags
WHERE NOT EXISTS (SELECT * FROM tagazations
WHERE tagazations.tag_id = tags.id
AND tagazations.project_id = projects.id)
AND tags.id IN (?))", tag_ids)
end
end
Alternatively, you can use count, group and having, although I suspect the first version is quicker but feel free to benchmark:
def with_tags(tag_ids)
joins(:tags).select('projects.*, count(tags.id) as tag_count')
.where(tags: { id: tag_ids }).group('projects.id')
.having('tag_count = ?', tag_ids.size)
end
This would be one way of doing it, although by no means the most efficient:
class Project < ActiveRecord::Base
has_many :tagazations
has_many :tags, :through => :tagazations
def find_with_all_tags(tag_names)
# First find the tags and join with their respective projects
matching_tags = Tag.includes(:projects).where(:name => tag_names)
# Find the intersection of these lists, using the ruby array intersection operator &
matching_tags.reduce([]) {|result, tag| result & tag.projects}
end
end
There may be a couple of typos in there, but you get the idea

Ruby on Rails - maximum count of associated objects?

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}

Rails: Query to get recent items based on the timestamp of a polymorphic association

I have the usual polymorphic associations for comments:
class Book < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
I'd like to be able to define Book.recently_commented, and Article.recently_commented based on the created_at timestamp on the comments. Right now I'm looking at a pretty ugly find_by_SQL query to do this with nested selects. It seems as though there must be a better way to do it in Rails without resorting to SQL.
Any ideas? Thanks.
For what it's worth, here's the SQL:
select * from
(select books.*,comments.created_at as comment_date
from books inner join comments on books.id = comments.commentable_id
where comments.commentable_type='Book' order by comment_date desc) as p
group by id order by null;
Sometimes it's just best to add a field to the object of which you are commenting. Like maybe a commented_at field of datetime type. When a comment is made on an object, simply update that value.
While it is possible to use SQL to do it, The commented_at method may prove to be much more scalable.
Not sure what your method has looked like previously but I'd start with:
class Book < ActiveRecord::Base
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', 5.minutes.ago])
end
end
This should find all the books that have had a comment created on them in the last 5 minutes. (You might want to add a limit too).
I'd also be tempted to create a base class for this functionality to avoid repeating the code:
class Commentable < ActiveRecord::Base
self.abstract_class = true
has_many :comments, :as => :commentable
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', Time.now - 5.minutes])
end
end
class Book < Commentable
end
class Article < Commentable
end
Also, you might want to look at using a plugin to achieve this. E.g. acts_as_commentable.

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Rails: order using a has_many/belongs_to relationship

I was wondering if it was possible to use the find method to order the results based on a class's has_many relationship with another class. e.g.
# has the columns id, name
class Dog < ActiveRecord::Base
has_many :dog_tags
end
# has the columns id, color, dog_id
class DogTags < ActiveRecord::Base
belongs_to :dog
end
and I would like to do something like this:
#result = DogTag.find(:all, :order => dog.name)
thank you.
In Rails 4 it should be done this way:
#result = DogTag.joins(:dog).order('dogs.name')
or with scope:
class DogTags < ActiveRecord::Base
belongs_to :dog
scope :ordered_by_dog_name, -> { joins(:dog).order('dogs.name') }
end
#result = DogTags.ordered_by_dog_name
The second is easier to mock in tests as controller doesn't have to know about model details.
You need to join the related table to the request.
#result = DogTag.find(:all, :joins => :dog, :order => 'dogs.name')
Note that dogs is plural in the :order statement.

Resources