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 %>
Related
I am a rails beginner and encountered the following issue
Models are setup as follows (many to many relation):
class Activity < ActiveRecord::Base
belongs_to :user
has_many :joinings
has_many :attendees, through: :joinings
end
class Joining < ActiveRecord::Base
belongs_to :activity
belongs_to :attendee
end
class Attendee < ActiveRecord::Base
has_many :joinings
has_many :activities, through: :joinings
end
This is one page test application for some users posting some activities, and other users to join the activities.
It is organized as single page format (activities index), and after each activity, there is a "Join" button users can click.
I am stuck at the point when a user needs to join a specific activity.
in the index.html.erb (of the activities), with the Join button code.
This will point to the attendee controller, to Create method, but I got no information regarding the Activity that I want to follow (eg. activity_id, or id)
Without this I cannot use the many to many relation to create the attendee.
What would be the correct button code, or any other suggestion to to get the corresponding activity ID in the attendees controller?
I tried a lot of alternatives, including even session[current_activity] , but is pointing (of course) always to the last activity.
Thanks so much !
If you have existing activities, and existing attendees, and you want to change the relationship between them, then you are editing the join table records. Therefore, you should have a controller for these. Like i said in my comment i'd strongly recomnmend renaming "joining" to "AttendeeActivity" so your schema looks like this:
class Activity < ActiveRecord::Base
belongs_to :user
has_many :attendee_activities
has_many :attendees, through: :attendee_activities
end
class AttendeeActivity < ActiveRecord::Base
belongs_to :activity
belongs_to :attendee
end
class Attendee < ActiveRecord::Base
has_many :attendee_activities
has_many :activities, through: :attendee_activities
end
Now, make an AttendeeActivitiesController, with the standard scaffoldy create/update/destroy methods.
Now, when you want to add an attendee to an activity, you're just creating an AttendeeActivity record. if you want to remove an attendee from an activity, you're destroying an AttendeeActivity record. Super simple.
EDIT
If you want to create an Attendee, and add them to an activity at the same time, then add a hidden field to the form triggered by the button:
<%= hidden_field_tag "attendee[activity_ids][]", activity.id %>
This will effectively call, when creating the attendee,
#attendee.activity_ids = [123]
thus adding them to activity 123 for example.
You have several options here. I'm assuming that the Join button will simply submit a hidden form to the attendees controller's create action. So the simplest solution would be to include the activity_id as a hidden form tag that gets submitted along with the rest of the form. This activity_id will be available in the params hash in the controller.
The other option is to setup Nested routing so that the activity_id is exposed via the path.
Thanks for all the details. I will change the naming of the join table for the future.
My problem was that I could not find the specific activity for attendee create method. Finally I found something like this for the JOIN button:
<%= button_to 'Join', attendees_path(:id => activity) %>
This will store the Activity ID, so I am able to find it in the Attendees controller:
def create
activity = Activity.find(params[:id])
#attendee = activity.attendees.create user_id: current_user.id
redirect_to activities_path
end
this updates also the Joinings (AttendeeActivity) table.
I will study about the hidden_field_tag solution, as is not clear to me yet.
I've been looking for the best method to do this quite some time with mediocre results so I decided to ask here.
The scenario is as follows: I have three models, Task, User and Comment, that essentially look like this:
class Task < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :task
end
class User < ActiveRecord::Base
has_many :tasks
has_many :comments
end
I'm trying to output a list of tasks (let's say last 10 for the purpose of this question), and associated last comment for each task and it's author (user model) with the least queries possible.
Thank you
UPDATE: I've combined solutions by Blue Smith and harigopal so the solution looks like this:
class Task < ActiveRecord::Base
belongs_to :user
has_many :comments
has_one :last_comment, -> { order 'created_at' }, class_name: "Comment"
end
and then fetch comments like this:
tasks = Task.joins(last_comment: :user)
.includes(last_comment: :user)
.order('tasks.created_at DESC').limit(10).load
which produces only one query, which was exactly what I was looking for! Thank you!
You'll have to join the tables, and eager-load the data to minimize number of queries.
For example:
tasks = Task.joins(comments: :user)
.includes(comments: :user)
.order('tasks.created_at DESC')
.limit(10).load
comments = tasks.first.comments # No query, eager-loaded
user = comments.first.user # No query, eager-loaded
This should reduce the number of queries to just one (very complex one), so you'll have to make sure your indexing is up to snuff! :-D
Official documentation about combining joins and includes is vague, but should help: http://guides.rubyonrails.org/active_record_querying.html#joining-multiple-associations
EDIT:
Here's a demo application with the same models as yours performing eager-loading using the above method. It uses two queries to load the data. Fire up the app to see it in action.
https://github.com/harigopal/activerecord-join-eager-loading
You could try something along the lines of this:
class Task
scope :recent, order('created_at desc')
scope :last, lambda{|n| limit: n }
end
Now you have reusable scopes:
Task.recent.last(10)
And i'm guessing you want to output the last 10 tasks for a given user (let's say the current logged in user).
current_user.tasks.recent.last(10).includes(comments: user)
You can try this:
class Task < ActiveRecord::Base
belongs_to :user
has_many :comments
has_one :last_comment, :class_name => 'Comment', :order => 'comments.created_at DESC'
end
#tasks = Task.limit(10).includes(:last_comment => :user)
last_comment is a "virtual" association and just load only one Comment record. The advantage of this approach is that it will use less memory (because just load one Comment and one related User). If you use includes(comments: user) it may consume a lot of memory (if there a lot of comments :).
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.
Each User can have many Resources, and each of those Resources has many Votes, and each of those votes have a value attribute that I want to sum all that particular users resources.
If I were to type this in a syntactically incorrect way I want something like...
#user.resources.votes.sum(&:value), but that obviously won't work.
I believe I need to use collect but I am not sure?
This is the closest I got but it prints them out, heh
<%= #user.resources.collect { |r| r.votes.sum(&:value) } %>
I'd recommend setting up a has_many :through relationship between the User and Vote objects. Set the models up like this:
class User < ActiveRecord::Base
has_many :resources
has_many :votes, :through => :resources
end
class Resource < ActiveRecord::Base
belongs_to :user
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :resource
end
Once this is done you can simply call user.votes and do whatever you want with that collection.
For more info on has_many :through relations, see this guide: http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
How can you tell who voted having a Vote instance? Your Vote model has to have voter_id field and additional association:
# in Vote.rb
belongs_to :voter, class_name: 'User', foreign_key: 'voter_id'
And in your User model:
# in User.rb
has_may :submited_votes, class_name: 'Vote', foreign_key: 'voter_id'
So, #user.votes (as David Underwood proposed) will give you #user resources' votes. And #user.submited_votes will give you votes submitted by the #user.
Using just User <- Resource <- Vote relation won't allow you to separate some user's votes made by him and votes made for its resources.
For a total sum this should work or something real close.
sum = 0
#user.resources.each do |r|
r.votes.each do |v|
sum += v.value
end
end
This might work for you:
#user.resources.map {|r| r.votes.sum(:value)}.sum
How many records do you have, there is a way to push this to the database level I believe, I would have to check, but if it is only a few records then doing this in ruby would probably be ok
Try this code
#user.resources.map(&:votes).flatten.map(&:value).sum
I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end