Scope topic query when ordering by comments - ruby-on-rails

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.

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.

Issue with polymorphic ActiveRecord query

I have three models with the following associations:
User has_many :owns, has_many :owned_books, :through => :owns, source: :book
Book has_many :owns
Own belongs_to :user, :counter_cache => true, belongs_to :book
I also have a page that tracks the top users by owns with the following query:
User.all.order('owns_count desc').limit(25)
I would now like to add a new page which can track top users by owns as above, but with a condition:
Book.where(:publisher => "Publisher #1")
What would be the most efficient way to do this?
I'm interesting if there is something special for this case, but my shot would be the following.
First, I don't see how polymorphic association can be applied here. You have just one object (user) that book can belong to. As I understand, polymorphic is for connecting book to several dif. objects (e.g. to User, Library, Shelf, etc.) (edit - initial text of question mentioned polymorphic associations, now it doesn't)
Second, I don't believe there is a way to cache counters here, as long as "Publisher #1" is a varying input parameter, and not a set of few pre-defined and known publishers (few constants).
Third, I would assume that amount of books by single Publisher is relatively limited. So even if you have millions of books in your table, amount of books per publisher should be hundreds maximum.
Then you can first query for all Publisher's books ids, e.g.
book_ids = Book.where(:publisher => "Publisher #1").pluck(:id)
And then query in owns table for top users ids:
Owns.select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order(total_owns: :desc).limit(25)
Disclaimer - I didn't try the statement in rails console, as I don't have your objects defined. I'm basing on group call in ActiveRecord docs
Edit. In order to make things more efficient, you can try the following:
0) Just in case, ensure you have indexes on Owns table for both foreign keys.
1) Use pluck for the second query as well not to create Own objects, although should not be a big difference because of limit(25). Something like this:
users_ids = Owns.where(book_id: book_ids).group(:user_id).order("count(*) DESC").limit(25).pluck("user_id")
See this question for reference.
2) Load all result users in one subsequent query and not N queries for each user
top_users = User.where(:id => users_ids)
3) Try joining User table in the first order:
owns_res = Owns.includes(:user).select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order("total_owns DESC").limit(25)
And then use owns_res.first.user

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.

Rails: Order array by number of child objects

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.

Rails: Sum of values in all Transactions that belong_to an Activity

Live site: http://iatidata.heroku.com
Github: https://github.com/markbrough/IATI-Data
Based on aid information released through the IATI Registry: iatiregistry.org
I'm a bit of a Rails n00b so sorry if this is a really stupid question.
There are two key Models in this app:
Activity - which contains details
such as recipient country, funding
organisation
Transaction - which contains details such as how much money (value) was committed or disbursed (transaction_type), when, to whom, etc.
All Transactions nest under an Activity. Each Activity has multiple Transactions. They are connected together by activity_id. has_many :transactions and belongs_to :activity are defined in the Activity and Transaction Models respectively.
So: all of this works great when I'm trying to get details of transactions for a single activity - either when looking at a single activity (activity->show) or looping through activities on the all activities page (activity->index). I just call
#activities.each do |activity|
activity.transactions.each do |transaction|
transaction.value # do something like display it
end
end
But what I now really want to do is to get the sum of all transactions for all activities (subject to :conditions for the activity).
What's the best way to do this? I guess I could do something like:
#totalvalue = 0
#activities.each do |activity|
activity.transactions.each do |transaction|
#totalvalue = #totalvalue + transaction.value
end
end
... but that doesn't seem very clean and making the server do unnecessary work. I figure it might be something to do with the model...?! sum() is another option maybe?
This has partly come about because I want to show the total amount going to each country for the nice bubbles on the front page :)
Thanks very much for any help!
Update:
Thanks for all the responses! So, this works now:
#thiscountry_activities.each do |a|
#thiscountry_value = #thiscountry_value + a.transactions.sum(:value)
end
But this doesn't work:
#thiscountry_value = #thiscountry_activities.transactions.sum(:value)
It gives this error:
undefined method `transactions' for #<Array:0xb5670038>
Looks like I have some sort of association problem. This is how the models are set up:
class Transaction < ActiveRecord::Base
belongs_to :activity
end
class Activity < ActiveRecord::Base
has_and_belongs_to_many :policy_markers
has_and_belongs_to_many :sectors
has_many :transactions
end
I think this is probably quite a simple problem, but I can't work out what's going on. The two models are connected together via id (in Activity) and activity_id (in Transactions).
Thanks again!
Use Active Record's awesome sum method, available for classes:
Transaction.sum(:value)
Or, like you want, associations:
activity.transactions.sum(:value)
Let the database do the work:
#total_value = Transaction.sum(:value)
This gives the total for all transactions. If you have some activities already loaded, you can filter them this way:
#total_value = Transaction.where(:activity_id => #activities.map(&:id)).sum(:value)
You can do it with one query:
#total_value = Transaction.joins(:activity).where("activities.name" => 'foo').sum(:value)
My code was getting pretty messy summing up virtual attributes. So I wrote this little method to do it for me. You just pass in a collection and a method name as a string or symbol and you get back a total. I hope someone finds this useful.
def vsum collection, v_attr # Totals the virtual attributes of a collection
total = 0
collection.each { |collect| total += collect.method(v_attr).call }
return total
end
# Example use
total_credits = vsum(Account.transactions, :credit)
Of course you don't need this if :credit is a table column. You are better off using the built in ActiveRecord method above. In my case i have a :quantity column that when positive is a :credit and negative is a :debit. Since :debit and :credit are not table columns they can't be summed using ActiveRecord.
As I understood, you would like to have the sum of all values of the transaction table. You can use SQL for that. I think it will be faster than doing it the Ruby way.
select sum(value) as transaction_value_sum from transaction;
You could do
#total_value = activity.transactions.sum(:value)
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html

Resources