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.
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'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.
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.
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.
I have a parent object, Post, which has the following children.
has_one :link
has_one :picture
has_one :code
These children are mutually exclusive.
Is there a way to use polymorphic associations in reverse so that I don't have to have link_id, picture_id, and code_id fields in my Post table?
I wrote up a small Gist showing how to do this:
https://gist.github.com/1242485
I believe you are looking for the :as option for has_one. It allows you to specify the name of the belongs_to association end.
When all else fails, read the docs: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_one
Is there a way to use polymorphic
associations in reverse so that I
don't have to have link_id,
picture_id, and code_id fields in my
Post table?
has_one implies that the foreign key is in the other table. If you've really defined your model this way, then you won't have link_id, picture_id, and code_id in your Post table. I think you meant to say belongs_to.
I want to do something like
#post.postable and get the child
object, which would be one of link,
picture, or code.
I believe you could do this by using STI and combining the links, pictures, and codes tables, then testing the type of the model when retrieving. That seems kludgey though, and could end up with lots of unused columns.
Is there a reason for not storing the unused id columns, other than saving space? If you're willing to keep them, then you could define a virtual attribute and a postable_type column : (untested code, may fail spectacularly)
def postable
self.send(self.postable_type)
end
def postable=(p)
self.send(postable_type.to_s+"=",p)
end