class Article
has_many :comments
end
class Comment
belongs_to :article
end
I'd like to be able to determine how many total comments exist for certain articles. For example: Article #20, #21 and #22 have a total of X comments between them.
Any pointers would be great!
I suggest this:
Comment.where(:article_id => [20, 21, 22]).count
Doing the counting etc. in the database and all in one query (which ActiveRecord will, in this case) is about as efficient as possible.
You could add a counter_cache to your articles table
class Article
has_many :comments, :counter_cache => true
end
and add a comments_count column to your articles table.
(http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) and then sum it as so.
Article.where(:id => [20,21,22]).sum(:comments_count)
You can easily do this without a counter cache if you prefer.
Article.where(:id => [20,21,22]).joins(:comments).count
Related
I'm learning my way around Rails and am working on a sample app to keep track of beer recipes.
I have a model called Recipe which holds the recipe name and efficiency.
I have a model called Ingredient which is using STI - this is subclassed into Malt, Hop, and Yeast.
Finally, to link the recipes and ingredients, I am using a join table called rec_items which holds the recipe_id, ingredient_id, and info particular to that recipe/ingredient combo, such as amount and boil time.
Everything seems to be working well - I can find all my malts by using Malt.all, and all ingredients by using Ingredient.all. I can find a recipe's ingredients using #recipe.ingredients, etc...
However, I'm working on my recipe's show view now, and am confused as to the best way to accomplish the below:
I want to display the recipe name and associated info, and then list the ingredients, but separated by ingredient type. So, if I have a Black IPA # 85% efficiency and it has 5 malts and 3 hops varieties, the output would be similar to:
BLACK IPA (85%)
Ingredient List
MALTS:
malt 1
malt 2
...
HOPS:
hop 1
...
Now, I can pull #recipe.rec_items and iterate through them, testing each rec_item.ingredient for type == "Malt", then do the same for the hops, but that doesn't seem very Rails-y nor efficient. So what is the best way to do this? I can use #recipe.ingredients.all to pull all the ingredients, but can't use #recipe.malts.all or #recipe.hops.all to pull just those types.
Is there a different syntax I should be using? Should I using #recipe.ingredient.find_by_type("Malt")? Doing this in the controller and passing the collection to the view, or doing it right in the view? Do I need to specify the has_many relationship in my Hop and Malt models as well?
I can get it working the way I want using conditional statements or find_by_type, but my emphasis is on doing this "the Rails way" with as little DB overhead as possible.
Thanks for the help!
Current bare-bones code:
Recipe.rb
class Recipe < ActiveRecord::Base
has_many :rec_items
has_many :ingredients, :through => :rec_items
end
Ingredient.rb
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
end
Malt.rb
class Malt < Ingredient
end
Hop.rb
class Hop < Ingredient
end
RecItem.rb
class RecItem < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingredient
end
recipes_controller.rb
class RecipesController < ApplicationController
def show
#recipe = Recipe.find(params[:id])
end
def index
#recipes = Recipe.all
end
end
Updated to add
I'm now unable to access the join table attributes, so I posted a new question:
Rails - using group_by and has_many :through and trying to access join table attributes
If anyone can help with that, I'd appreciate it!!
It's been a while since I've used STI, having been burned a time or two. So I may be skipping over some STI-fu that would make this easier. That said...
There are many ways of doing this. First, you could make a scope for each of malt, hops, and yeast.
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
named_scope :malt, :conditions => {:type => 'Malt'}
named_scope :hops, :conditions => {:type => 'Hops'}
...
end
This will allow you to do something line:
malts = #recipe.ingredients.malt
hops = #recipe.ingedients.hops
While this is convenient, it isn't the most efficient thing to do, database-wise. We'd have to do three queries to get all three types.
So if we're not talking a ton of ingredients per recipe, it'll probably be better to just pull in all #recipe.ingredients, then group them with something like:
ingredients = #recipe.ingredients.group_by(&:type)
This will perform one query and then group them into a hash in ruby memory. The hash will be keyed off of type and look something like:
{"Malt" => [first_malt, second_malt],
"Hops" => [first_hops],
"Yeast" => [etc]
}
You can then refer to that collection to display the items however you wish.
ingredients["Malt"].each {|malt| malt.foo }
You can use group_by here.
recipe.ingredients.group_by {|i| i.type}.each do |type, ingredients|
puts type
ingredients.each do |ingredient|
puts ingredient.inspect
end
end
The utility of STI in this instance is dubious. You might be better off with a straight-forward categorization:
class Ingredient < ActiveRecord::Base
belongs_to :ingredient_type
has_many :rec_items
has_many :recipes, :through => :rec_items
end
The IngredientType defines your various types and ends up being a numerical constant from that point forward.
When you're trying to display a list this becomes easier. I usually prefer to pull out the intermediate records directly, then join out as required:
RecItem.sort('recipe_id, ingredient_type_id').includes(:recipe, :ingredient).all
Something like that gives you the flexibility to sort and group as required. You can adjust the sort conditions to get the right ordering. This might also work with STI if you sort on the type column.
I'm loving Rails but we just started dating.
A user can vote on both links and comments. In addition to primary key and timestamp, I currently have the following attributes defined for these models:
Link url, headline, submitter_id, score
Comment content, commenter_id, score, link_id, parent_comment_id
Vote id, voter_id, link_id, direction
I just added the Comment Model and thinking through how to integrate it with votes. Some options:
Collapse links and comments into a single "Item" model, and map votes to the generic item_id
Have two vote tables, one for comments, one for links
Add comment_id column to existing Vote table
Not sure what's best. #1 and #3 introduce dual-purpose tables, i.e. there are certain columns in a table that are only relevant to subsets of rows within that table. #2 avoids this problem, but seems redundant and silly.
Is the tradeoff inevitable or am I not seeing the golden path? What would you recommend? And if you happen to know of a rails repository on github that handles a similar situation, I'd really appreciate a link!
I think what you are looking for is a polymorphic association. In your case would be as simple as :
class Vote < ActiveRecord::Base
belongs_to :votable, :polymorphic => true
end
class Link < ActiveRecord::Base
has_many :votes, :as => :votable
end
class Comment < ActiveRecord::Base
has_many :votes, :as => :votable
#...
end
Your votes table should look like:
id : integer
votable_id : integer
votable_type : string # Comment || Link
Here you have a Railscast about it: Polymorphic Associations Railscast
How do I eager-load only some of the objects in a has_many relationship?
Basically, I have four objects: Assignment, Problem, AssignmentUser, and ProblemUser.
#assignment.rb
has_many :problems
has_many :assignment_users
#assignment_user.rb
belongs_to :assignment
belongs_to :user
has_many :problem_users
#problem.rb
belongs_to :assignment
has_many :problem_users
#problem_user.rb
belongs_to :user
belongs_to :problem
belongs_to :assignment_user
attr_accessor :complete #boolean
On a page showing a single assignment, I want to show all of the problems, as well the user's status on each problem, if it exists. (It might not, if this is the first time the user is viewing the page.)
I can't call assignment_user.problem_users and then snake the problems out like so:
-#assignment_user.problem_users.each do |problem_user|
= check_box_tag "problems[#{problem_user.problem.id}]", 1, problem_user.complete
= link_to problem_user.problem.name, assignment_problem_path(assignment_id => problem_user.problem.assignment_id, :id => problem_user.problem_id)
because there might not be ProblemUser entries for every Problem that belongs to an assignment; creating all of those ProblemUser objects whenever someone creates a Problem object would be wasteful, so they're only created on the fly.
What I want is to be able to iterate over the Problems that belong to the particular Assignment, then for each Problem, find a ProblemUser that matches...but without creating an N+1 problem. I could create two arrays, one with all of the problems and one with all of the problem_users, but then I would have to match them up, right? Maybe that's the best way... but any recommendations on best practices are appreciated here.
Try using :include something along the lines of...
#assignment.rb
has_many :problems, :include => :problem_user
has_many :assignment_users
Assuming a field named description in each of the tables assignments, problems, and problem_users the solution should resemble this...
Assignment.find(1).problems.collect { |a| [a.assignment.description, a.description, a.problem_user.description] }
Simple task: given that an article has many comments, be able to display in a long list of articles how many comments each article has. I'm trying to work out how to preload this data with Arel.
The "Complex Aggregations" section of the README file seems to discuss that type of situation, but it doesn't exactly offer sample code, nor does it offer a way to do it in two queries instead of one joined query, which is worse for performance.
Given the following:
class Article
has_many :comments
end
class Comment
belongs_to :article
end
How can I preload for an article set how many comments each has?
Can't you use counter cache for this?
belongs_to :article, :counter_cache => true
You also need to have a migration that adds the column comments_count
You can do something nasty using SQL like:
default_scope :select => 'articles.*, (select count(comments.id) from comments where comments.article_id = articles.id) as count_comments'
and then you would have access to Article.first.count_comments.
Another (nicer) method to do it is to use the 'counter_cache' feature/option from belongs_to association.
Okay i have two models: posts and comments.
as you can think comments has column :post_id.
My models
Comments
belongs_to :post
Post
has_many :comments
So, this is pretty simple association but i have some problems with ordering comments.
at first time, when i create my comments migration file i just add column :position.
This column indicate comment position in the post.
But now i think what where is more good way to do this.
so i can't make my choise:
1) uses t.column :datatime :created_at, :default => Time.now()
2) or use timestamps? this is undiscovered for me, please tell me about your exp.
yar is right. No need to have a position column, unless you'll want to reorder your comments, which I doubt you will :) Just do
has_many :comments, :order => "created_at DESC"
and every time you use #post.comments they will be sorted by date.
In Rails, all models automatically get a created_at field. You can just order by that.