I was recently following http://railscasts.com/episodes/228-sortable-table-columns to add sortable table columns to my app. It worked great, but some of the columns in my table are things like post.comments.count (obviously posts have many comments). I'd like to be able to sort by the number of comments a post has in the table, but I can't figure out how I'd implement this using the solution in the railscast.
The easiest way is to use a counter_cache.
Using a migration, create a comments_count:integer database field on table posts.
Then update your model:
class Comment < ActiveRecord::Base
belongs_to :post, :counter_cache => true
end
Then sort on that column:
Post.order(:comments_count)
One way would be a counter cache column in the case you had mentioned
post.comments_count
# instead of
post.comments.count
Not sure if that will be applicable in every of your cases.
Related
class Post
has_many :commments
end
class Comment
belongs_to :post
end
I wish to display a list of posts ordered by date of post creation (submitted_at). I also want some post xyz to appear at the top if it has some new comment posted and yet to be reviewed by moderator. We will determine this by a boolean attribute/field at comments level (moderated = 1/0)
I tried
Posts.join(:comments)
.distinct
.order("submitted_at DESC, comments.moderated")
but this excludes posts that have no comments and results aren't sorted as expected. I am sure that we can do this at ruby level, but looking for a way to do this using AR.
For the join, use this:
Posts.join("LEFT JOIN comments ON comments.post_id = posts.id")
Which will include the ones with no comments.
Your sorting seems to suggest you want a count of moderated comments, in which case, try this:
.order("submitted_at DESC, COUNT(comments.moderated)")
Although, you may need to use group in some way too.
I have a Track table and a Section table. A track has many sections. Sections are connected to their respective task by the Section's :track_id, which corresponds to the Track's :id attribute.
<% #track = Track.find(params[:id]) %>
<% #sections = Section.find_by_track_id(#track.id) %>
In the code above I'm trying to find multiple sections that share the same :track_id attribute, but find_by_track_id() only returns the first. What's the best way to get all of them?
Thanks!
If your tracks and sections are related in this way, then the best way to relate them is by using the methods that come automatically from Rails' associations.
in this case, I expect in your model files, you have the following:
class Track < ActiveRecord::Base
has_many :sections
end
class Section < ActiveRecord::Base
belongs_to :track
end
Then you can get the sections for a track like this:
#track = Track.find(params[:id])
#sections = #track.sections
You're looking for where, which finds all records where a specific set of conditions are met.
#sections = Section.where(track_id: #track.id)
This is unrelated to your question, but you should set #sections and #track in your controller. As it seems like you're new to Rails, I'd highly recommend reading through the Rails Guides. They will help you immensely on your journey.
EDIT: I was solving for the general question of "Find multiple database objects by attribute in Rails?", which is how to find multiple database objects in the general case. #TarynEast's method is the way to go to find all of the sections for a track, or more generally, all of the objects that belong to the desired object. For the specific case you're asking for above, go with #TarynEast's solution.
Association
To extend Taryn East's answer, you need to look into ActiveRecord Associations.
In your model, if you have the following has_many relationship:
#app/models/track.rb
Class Track < ActiveRecord::Base
has_many :sections
end
#app/models/section.rb
Class Section < ActiveRecord::Base
belongs_to :track
end
This will set up a relational database association between your tracks and sections datatables.
--
Associative Data
The magic of Rails comes into play here
When you call the "parent" object, you'll be able to locate it using its primary key (typically the ID). The magic happens when Rails automatically uses this primary_key as a foreign_key of the child model - allowing you to call all its data as an append to the parent object:
#track = Track.find params[:id] #-> find single Track by primary key
#sections = #track.sections #-> automagically finds sections using the track primary key
This means if you call the following, it will work exactly how you want:
#sections.each do |section|
section.name
end
Where
Finally, if you wanted to look up more than one record at a time, you should identify which ActiveRecord method you should use:
find is to locate a single record by id
finy_by key: "value" is to locate a single record by your defined key/column
where is to return multiple items using your own conditions
So to answer your base line question, you'll want to use where:
#sections = Section.where track_id: params[:id]
This is not the right answer, but it should help you
<% #sections=#track.sections%>
Use find when you are looking for one specific element identified by it's id.
Model.find is using the primary key column. Therefore there is always exactly one or no result.
I have an array of posts. I want to order this array of posts by number of comments they have (First object in the array being most comments, last being the least). A Post has many comments. This seems like a simple problem, but I can't figure out how to order them. I've tried and I don't believe it's possible to achieve this via the order method. Is there a Rails method I don't know about? Or will I have to home roll it? Thanks in advance!
PS: My relation of comments to posts is polymorphic Comment - (belongs_to :threadable)
You can do this with a complicated active record query, however, the easiest thing by far is to just add a :counter_cache on association. You just add a field on your Post model called comments_count and then in your Comment model:
belongs_to :post, counter_cache: true
Then since it's just a column like all your other columns you can order by it.
See: http://guides.rubyonrails.org/association_basics.html
The easiest way to do this would probably be using the counter_cache option
see:
http://guides.rubyonrails.org/association_basics.html#counter-cache
http://railscasts.com/episodes/23-counter-cache-column
-
class Comment
belongs_to :post, counter_cache: true
end
add the column comments_count to the posts database table, then order by that
The best way to achieve it is to use counter cache feature. You need to add integer column to your posts table called comments_count. Then on your comments model you need to change your post association to read:
belongs_to :post, counter_cache: true
Rails will handle comments_count column to show how many comments are associated with given post. You can then use this column for ordering.
I'm trying to set the order for Topic index by most recent Comment. This is working:
Topic.joins(:comments).order("comments.created_at desc")
But it lists the Topics more than one time.
Is there a way to limit the times each topic is displayed to one?
Ok, what's happening is this: When you join the comments, you get one database row for each comment, which (if there are multiple comments on a Topic), means multiple copies of each topic record.
To fix this, you'll need to use grouping, so that there's only one result per topic. The thing to group by is the id of the model you're returning (topics.id). Now, there's more to it - because there are still multiple comments per result, there are also multiple created_at values for each, and thus (in order to sort by it, as you do) you need to tell the database which one to use.
This is done with an aggregation function of some sort. I'm guessing you want the most recent comment to be the one that determines the order. If that's true, the code will be something like this:
Topic.joins(:comments).select('topics.*, max(comments.created_at) as last_comment').group('topics.id').order('last_comment desc')
The custom select includes the usual data that you need (everything about the Topic object - topics.*) and also an aggregate function (max(), which as the name suggests returns the largest of the possible values. ) used on the creation date of the comments. More recent dates are larger, so this will be the most recent comment's creation datestamp. That result is aliased as last_comment (using as), so you can refer to it in the .order call.
The most elegant way I found to solve the problem is to add touch to the polymorphic association in the comment model:
belongs_to :commentable, :polymorphic => true, touch: true
If you're not using a polymorphic association, you can use:
belongs_to :topic, touch: true
Then, all I had to do was change the default scope in the topic model to updated_at
default_scope order: 'topics.updated_at DESC'
Adding touch to comment means that every time a comment is added to topic, it updates the updated_at column in topics.
In the controller I am using:
#topics = Topic.order(sort_column + " " + sort_direction).paginate(:per_page => 20, :page => params[:page])
Topic.all or something else could work there as well.
Thanks to my super friend Alain for pointing this out to me.
My application has about half a dozen different types of items that a user can comment on (articles, photos, videos, user profiles, blog posts, forum posts).
My plan right now is to have a single comments table in my database and then have parent_id and type fields in the table.
The type field would just be a string and the contents of it would be the name of it's parent table. So for an Article comments, the type would be article, for example.
Is that the best way to handle the comments table? Or is there some other, more efficient way to do that?
Using a polymorphic association would be the best way to achieve this - check out Ryan's railscast on the subject (or the ASCIIcast at http://asciicasts.com/episodes/154-polymorphic-association). I think you'll end up with something like:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
belongs_to :user
end
Then each model you want users to be able to comment on will have the line:
has_many :comments, :as => :commentable
I also found this post useful when setting up polymorphic comments. Hope that helps!
What you are describing is a called a polymorphic association. Rails can handle that out of the box, see this episode:
http://railscasts.com/episodes/154-polymorphic-association
If a comment can't apply to multiple things (the same comment can't apply to both an article and a blog post, or two different articles) then why is it a base entity?
If you're committed to it, I'd have a comment table that looked like this:
COMMENT_ID
COMMENT_BODY
USER_ID
DATE
ARTICLE_ID references ARTICLE on delete cascade
BLOG_POST_ID references BLOG_POST on delete cascade
... etc
And then have constraint that says one and only one of the parents can apply.
An alternative is to have a COMMENT table for each base entity, so you'd have ARTICLE_COMMENTS, BLOG_POST_COMMENTS, and so forth.
I'd suggest looking at plugins like acts_as_commentable, and looking at how they are implemented. Or just use the plugin.
I think your design is pretty good. It is simple and easy to implement. The only requirement would be that the data-types of the row identifiers would have to be the same as comment.parent_id so that your joins are consistent. I would actually define views on the comments table for each 'type'. For example, "create photo_comments as select * from comments where type = 'PHOTOS'". then you could join from PHOTOS to PHOTO_COMMENTS on PHOTOS.PHOTO_ID = PHOTO_COMMENTS.PARENT_ID, and so on.
You could also create a supertype say, widget, and each of your photo, blog_post etc. could be sub-types of widget. Then you could constrain your comments table to have an FK to the widget table. The 'type' column could then be moved to the widget table.