Get last comment of all posts in one category - ruby-on-rails

Comment belongs to Post.
Post belongs to Category.
How would I get a collection of every lastly updated comment for each post, all belonging to one single category?
I've tried this but it just gives me one post:
category.posts.joins(:comments).order('updated_at DESC').first
Update
What I want is to fetch one commment per post, the last updated comment for each post.

Rails doesn't do this particularly well, especially with Postgres which forbids the obvious solution (as given by #Jon and #Deefour).
Here's the solution I've used, translated to your example domain:
class Comment < ActiveRecord::Base
scope :most_recent, -> { joins(
"INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)"
)}
...
(DISTINCT ON is a Postgres extension to the SQL standard so it won't work on other databases.)
Brief explanation: the DISTINCT ON gets rid of all the rows except the first one for each post_id. It decides which row the first one is by using the ORDER BY, which has to start with post_id and then orders by updated at DESC to get the most recent, and then id as a tie-breaker (usually not necessary).
Then you would use it like this:
Comment.most_recent.joins(:post).where("posts.category_id" => category.id)
The query it generates is something like:
SELECT *
FROM comments
INNER JOIN posts ON (posts.id=comments.post_id)
INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)
WHERE
posts.category_id=#{category.id}
Single query, pretty efficient. I'd be ecstatic if someone could give me a less complex solution though!

If you want a collection of every last updated Comment, you need to base your query on Comment, not Category.
Comment.joins(:post).
where("posts.category_id = ?", category.id).
group("posts.id").
order("comments.updated_at desc")

What you're basically asking for is a has_many :through association.
Try setting up your Category model something like this:
class Category < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
end
Then you can simply do this to get the last 10 updated comments:
category.comments.order('updated_at DESC').limit(10)
You could make this more readable with a named scope on your Comment model:
class Comment < ActiveRecord::Base
scope :recently_updated, -> { order('updated_at DESC').limit(10) }
end
Giving you this query to use to get the same 10 comments:
category.comments.recently_updated
EDIT
So, a similar solution for what you actually wanted to ask for, however it requires you to approach your associations from the Comment end of things.
First of all, set up an association on Comment so that it has knowledge of its Category:
class Comment < ActiveRecord::Base
belongs_to :post
has_one :category, through: :post
end
Now you can query your comments like so:
Comment.order('updated_at desc').joins(:post).where('posts.category' => category).group(:post_id)
Somewhat long-winded, but it works.

.first is grabbing only one for you. The first one to be exact. So drop the .first. So instead do:
category.posts.joins(:comments).order('updated_at DESC')

Related

has_one association not working with includes

I've been trying to figure out some odd behavior when combining a has_one association and includes.
class Post < ApplicationRecord
has_many :comments
has_one :latest_comment, -> { order('comments.id DESC').limit(1) }, class_name: 'Comment'
end
class Comment < ApplicationRecord
belongs_to :post
end
To test this I created two posts with two comments each. Here are some rails console commands that show the odd behavior. When we use includes then it ignores the order of the latest_comment association.
posts = Post.includes(:latest_comment).references(:latest_comment)
posts.map {|p| p.latest_comment.id}
=> [1, 3]
posts.map {|p| p.comments.last.id}
=> [2, 4]
I would expect these commands to have the same output. posts.map {|p| p.latest_comment.id} should return [2, 4]. I can't use the second command because of n+1 query problems.
If you call the latest comment individually (similar to comments.last above) then things work as expected.
[Post.first.latest_comment.id, Post.last.latest_comment.id]
=> [2, 4]
If you have another way of achieving this behavior I'd welcome the input. This one is baffling me.
I think the cleanest way to make this work with PostgreSQL is to use a database view to back your has_one :latest_comment association. A database view is, more or less, a named query that acts like a read-only table.
There are three broad choices here:
Use lots of queries: one to get the posts and then one for each post to get its latest comment.
Denormalize the latest comment into the post or its own table.
Use a window function to peel off the latest comments from the comments table.
(1) is what we're trying to avoid. (2) tends to lead to a cascade of over-complications and bugs. (3) is nice because it lets the database do what it does well (manage and query data) but ActiveRecord has a limited understanding of SQL so a little extra machinery is needed to make it behave.
We can use the row_number window function to find the latest comment per-post:
select *
from (
select comments.*,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt.rn = 1
Play with the inner query in psql and you should see what row_number() is doing.
If we wrap that query in a latest_comments view and stick a LatestComment model in front of it, you can has_one :latest_comment and things will work. Of course, it isn't quite that easy:
ActiveRecord doesn't understand views in migrations so you can try to use something like scenic or switch from schema.rb to structure.sql.
Create the view:
class CreateLatestComments < ActiveRecord::Migration[5.2]
def up
connection.execute(%q(
create view latest_comments (id, post_id, created_at, ...) as
select id, post_id, created_at, ...
from (
select id, post_id, created_at, ...,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt. rn = 1
))
end
def down
connection.execute('drop view latest_comments')
end
end
That will look more like a normal Rails migration if you're using scenic. I don't know the structure of your comments table, hence all the ...s in there; you can use select * if you prefer and don't mind the stray rn column in your LatestComment. You might want to review your indexes on comments to make this query more efficient but you'd be doing that sooner or later anyway.
Create the model and don't forget to manually set the primary key or includes and references won't preload anything (but preload will):
class LatestComment < ApplicationRecord
self.primary_key = :id
belongs_to :post
end
Simplify your existing has_one to just:
has_one :latest_comment
Maybe add a quick test to your test suite to make sure that Comment and LatestComment have the same columns. The view won't automatically update itself as the comments table changes but a simple test will serve as a reminder.
When someone complains about "logic in the database", tell them to take their dogma elsewhere as you have work to do.
Just so it doesn't get lost in the comments, your main problem is that you're abusing the scope argument in the has_one association. When you say something like this:
Post.includes(:latest_comment).references(:latest_comment)
the scope argument to has_one ends up in the join condition of the LEFT JOIN that includes and references add to the query. ORDER BY doesn't make sense in a join condition so ActiveRecord doesn't include it and your association falls apart. You can't make the scope instance-dependent (i.e. ->(post) { some_query_with_post_in_a_where... }) to get a WHERE clause into the join condition, then ActiveRecord will give you an ArgumentError because ActiveRecord doesn't know how to use an instance-dependent scope with includes and references.

Rails: Order activerecord object by attribute under has_many relation

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.

Query that joins child model results item erroneously shown multiple times

I have the following models, each a related child of the previous one (I excluded other model methods and declarations for brevity):
class Course < ActiveRecord::Base
has_many :questions
scope :most_answered, joins(:questions).order('questions.answers_count DESC') #this is the query causing issues
end
class Question < ActiveRecord::Base
belongs_to :course, :counter_cache => true
has_many: :answers
end
class Answer < ActiveRecord::Base
belongs_to :question, :counter_cache => true
end
Right now I only have one Course populated (so when I run in console Course.all.count, I get 1). The first Course currently has three questions populated, but when I run Course.most_answered.count (most_answered is my scope method written in Course as seen above), I get 3 as the result in console, which is incorrect. I have tried various iterations of the query, as well as consulting the Rails guide on queries, but can't seem to figure out what Im doing wrong. Thanks in advance.
From what I can gather, your most_answered scope is attempting to order by the sum of questions.answer_count.
As it is there is no sum, and since there are three answers for the first course, your join on to that table will produce three results.
What you will need to do is something like the following:
scope :most_answered, joins(:questions).order('questions.answers_count DESC')
.select("courses.id, courses.name, ..., SUM(questions.answers_count) as answers_count")
.group("courses.id, courses.name, ...")
.order("answers_count DESC")
You'll need to explicitely specify the courses fields you want to select so that you can use them in the group by clause.
Edit:
Both places where I mention courses.id, courses.name, ... (in the select and the group), you'll need to replace this with the actual columns you want to select. Since this is a scope it would be best to select all fields in the courses table, but you will need to specify them individually.

Rails - ActiveRecord query - Find first item where relationship.count at least 1

I have two models:
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
I'm attempting to find the most recent Post that has at least one Comment.
I tried this:
#post = Post.find_by_sql("SELECT *, count(comment.id) AS num_of_comments
FROM post
INNER JOIN comment ON post.id = comment.post_id
WHERE num_of_comments >= 1").last
but got an error saying num_of_comments was an unknown column.
Then I tried a method on Post, but that didn't work:
def self.has_been_commented
where("comments.count <= 1")
end
I then started looking into scopes, and saw the .joins method, but wasn't sure how I could then specify another filter.
Something like:
scope :has_been_commented, joins(:comments)
but then I'm not sure how to specify where(:comments.count >= 1)
Sorry if that is confusing...
You have plural comments and singular comment liberally mixed together. If you are following "the Rails way" then everything would be plural, even the database name, except your class name. The goal is to make sure the SQL being generated matches the database table you have.
I found the answer to my question:
Post.find(:all, :joins => "INNER JOIN comment ON comment.post_id = post.id", .
:select => "post.*, count(comment.id) comment_count",
:group => "comment.post_id HAVING comment_count >= 1").last

ordering by child's value in one to many relationship

posts table
int id
comments table
int id
int post_id
datetime created_at
i need to order posts by post's comments created_at. I tried something like that but i can't select distinct post ids.Any help will be appreciated.
Post.all(:joins => :comments, :order => 'comments.created_at')
I want to see the post which was commented lately at the top.
The condition you are passing is invalid.
The easiest way you can accomplish what you are trying to do, is by adding a column to your posts table - say last_commented_at.
In your Comment model you add a callback
class Comment < ActiveRecord::Base
belongs_to :post
after_save :update_posts_last_commented_attribute
def update_posts_last_commented_attribute
post.update_attribute(:last_commented_at, updated_at)
end
end
Then you can load your Posts by calling
Post.order("last_commented_at DESC" )
to show the posts first, that have recently been commented.

Resources