I have an application with Users, Posts and Comments. Users has_many Posts, Posts has_many Comments and Comments belong_to Users and Posts.
In my view template I'm looping over the Comments like this to get the username of the commenter:
User.find(comment.user_id).name
How efficient is this if I'm doing this for every Comment per Post ?
I could imagine that it's maybe faster to store the username in a separate row in the Comments table, but duplicating the data seems just wrong.
Am I being paranoid and is ActiveRecord doing some caching magic or is there a better way of doing something like this ?
You can preload the users in the initial query.
Seeing as you have the association setup between User and Comment you can access the user via the association.
Let's assume you have a controller method:
def show
#posts = Post.limit(10).includes(:comments => :user)
end
In your view as you loop over #comments:
#posts.each do |post|
# show post stuff
post.comments.each do |comment|
comment.user.name
end
end
Revised
If you have association as below you can access user from comment object itself comment.user
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Eager loading of associations
class Post < ActiveRecord::Base
has_many :comments
# ...
end
class Comment < ActiveRecord::Base
belongs_to :user
end
comments = #post.comments.includes(:user)
It will preload users.
SQL will look like:
SELECT * FROM `comments` WHERE `comments`.`post_id` = 42;
SELECT * FROM `users` WHERE `users`.`id` IN (1,2,3,4,5,6) # 1,2,3,4,5,6 - user_ids from comments
If performance is an issue for you, using a none relational DB, like Mongodb is the way to go.
If you still want to use ActiveRecord, either you use eager loading with Post.comments.include(:user) (which will load unused users info - and that's not great), or you can use caching techniques.
I find it OK to cache the user.name in the comments table, as you suggested, as long as you control the changes that can occur. You can do that by setting callbacks in your User model:
after_save do |user|
Comment.where(user_id: user.id).update_all(:username, user.name)
end
This technique is sort of a DB caching, but, of course, you can cache HTML fragments. And a comment block is a good to cache HTML block.
Related
Let's say I have two models, Post and Comment. Post has_many Comments.
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Suppose I want to get a list of Post, Comment pairs. I could do this:
Post.includes(:comments).each do |post|
post.comments.each do |comment|
# do something with post and comment
end
end
But let's say that for whatever reason, maybe to do one less database query, I do a join:
Post.joins(:comments)
# => [#<Post:0xblahblah>]
Given one of the Posts returned by the above query, how do I access the Comment that the Post was joined with?
ps: for eager load, use includes instead of joins. You can access a certain post comments directly, by post.comments; or access a comment post by comment.post
I've got a dilemma I think I may have coded myself into a corner over. Here's the setup.
My site has users. Each user has a collection of stories that they post. And each story has a collection of comments from other users.
I want to display on the User's page, a count of the total number of comments from other users.
So a User has_many Stories, and a Story has_many comments.
What I tried was loading all the users stories in #stories and then displaying #stories.comments.count, but I get undefined method 'comments' when I try to do that. Is there an efficient ActiveRecord way to do this?
class User < ActiveRecord::Base
has_many :stories
has_many :comments, through: :stories
end
class Story < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :story
end
Now you should be able to get User.last.comments.count
I think you need to refine this more for a proper labelling.
The quick solution is to iterate over the #stories collection and add the counts up. This is not a purely active record solution though.
totalComments = 0
#stories.each do |story|
totalComments += story.count
end
For a pure active record solution I would need to assume that each has_many association has a corresponding belongs_to association. So a User has_many Stories and a Story belongs_to a User. If that is the case and comments have a similar association to stories then you can search comments by user_id. Something like:
Comments.where("comment.story.user" => "userId")
I hope that helps.
In your controller you should have something like this (note the use of includes):
#user = User.find( params[:id] )
#stories = #user.stories.includes(:comments)
Then in your view you can do something like the following to show the total number of comments for that particular user:
Total number of comments: <%= #stories.map{|story| story.comments.length}.sum %>
I am using Ruby on Rails 4 and I would like to eager load associated objects for a has_many association. That is, I have the following models:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end
Since I frequently retrieve comments when I load articles I would like to eager load comments each time I search for articles, for example in these cases:
# In these cases the eager loading should happen.
Article.find(1)
Article.where(:title => 'Sample text')
#current_user.articles
What is the proper way to handle this issue? How?
You should use includes method, like in following:
Article.includes(:comments).where(title: 'Sample text')
For your first case, you don't really need eager loading, since you only select one record, so this is not n+1 problem. You can just do:
article = Article.find(1)
article.comments
If you use include your articles frequently, you may define scope:
scope :with_comments, -> { includes(:comments }
and simply call it:
Article.with_comments.where(title: 'Sample text')
When your relation into which you want to include comments comes from another association, nothing changes, you can do both:
#current_user.articles.includes(:comments)
and
#current_user.articles.with_comments
If you want comments to be eager loaded automatically whenever you load article, you can use default_scope on Article model
class Article < ActiveRecord::Base
has_many :comments
default_scope :include => :comments
end
I'm having a strange issue.
I have a number of models and associations that work perfectly together but when I try to introduce the current_user I get:
ActiveRecord::ConfigurationError at /dashboard
Message Association named 'game' was not found; perhaps you misspelled it?
Here's some code that works:
Controller:
def index
users = current_user.followed_users.collect { |user| user.id }
#userupdates = Userupdate.for_users(users, params[:page])
end
View:
<% #userupdates.each do |userupdate| %>
Things and stuff
<% end %>
But when I try to make the page display content from followed_users AND the current_user like so..
def index
users = current_user.followed_users.collect { |user| user.id }
users.push(current_user.id)
#userupdates = Userupdate.for_users(users, params[:page])
end
...I get the error above.
Some of the relavent model code:
class Userupdate < ActiveRecord::Base
belongs_to :user
belongs_to :game
class User < ActiveRecord::Base
has_many :userupdates
class Game < ActiveRecord::Base
has_many :userupdates
ActiveRecord::ConfigurationError is explained as below in rails api.
Raised when association is being configured improperly or user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
So I thought the problem is your association configured. You may check again the association, like whether model Userupdate has user_id and game_id.
And the current_user issue, maybe you should check your query sql to see whether your includes works. If works, it should do the outer join between userupdates and users, userupdates and games, and you'll see loading users and games after loading userupdates in log. And current_user maybe the only user who has the userupdates whose belonging game exists.
All my opinions, hope this can help.
A Post belongs_to a User, and a User has_many Posts.
A Post also belongs_to a Topic, and a Topic has_many Posts.
class User < ActiveRecord::Base
has_many :posts
end
class Topic < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :topic
end
Well, that's pretty simple and very easy to set up, but when I display a Topic, I not only want all of the Posts for that Topic, but also the user_name and the user_photo of the User that made that Post. However, those attributes are stored in the User model and not tied to the Topic. So how would I go about setting that up?
Maybe it can already be called since the Post model has two foreign keys, one for the User and one for the Topic?
Or, maybe this is some sort of "one-way" has_many through assiociation. Like the Post would be the join model, and a Topic would has_many :users, :through => :posts. But the reverse of this is not true. Like a User does NOT has_many :topics.
So would this even need to be has_many :though association? I guess I'm just a little confused on what the controller would look like to call both the Post and the User of that Post for a give Topic.
Edit: Seriously, thank you to all that weighed in. I chose tal's answer because I used his code for my controller; however, I could have just as easily chosen either j.'s or tim's instead. Thank you both as well. This was so damn simple to implement. I think today might mark the beginning of my love affair with rails.
you can get the user_name and user_photo when displaying a topic and its posts...
something like:
#topic.posts.each do |post|
user_name = post.user.name
user_photo = post.user.photo
end
it's simple. sorry if i didn't understand your question very well.
Well, if what you want is display the user name of the author of a post, for example, you can do something like (not tested, but should work) :
#posts = topic.posts.all(:include => :user)
it should generate one request for all posts, and one for users, and the posts collection should have users.
in a view (haml here) :
- #posts.each do |post|
= post.user.name
= post.body
If I understand your question correctly, no, a has_many :through association is not required here.
If you wanted to display a list of all users that posted in a topic (skipping the post information), then it would be helpful.
You'll want to eager load the users from posts, though, to prevent n+1 queries.