Rails: Reordering an ActiveRecord result set - ruby-on-rails

I want to retrieve the last five Articles by review date, but I want to display them in chronological order (ascending order). How do I do this?
# controller
#recent_articles = Article.where("reviewed = ?", true).order("review_date DESC").limit(5)
# view
<% #recent_articles.each do |ra| %>
# articles sorted by review_date in ascending order
<% end %>
I tried doing this in the controller, but it retrieved the five oldest review_dates instead (i.e. it just retrieved the records by ASC order instead of reordering the array already retrieved):
#recent_articles = Article.where("reviewed = ?", true).order("review_date DESC").limit(5)
#recent_articles = #recent_articles.reorder("review_date ASC")

So you want the articles from the DESC query but in the reverse order that they come in from the database?
#recent_articles.reverse!

I'd suggest you create a reviewed and recent scopes in your model. This will allow nicely chain-able scopes in your controllers:
# article.rb
scope :reviewed, lambda { where("reviewed = ?", true) }
scope :recent, lambda { |n| order("review_date").last(n) }
# in your controller..
#recent_articles = Article.reviewed.recent(5)
Notice I'm using last(n), not limit. There is no point in ordering by review_date desc just to get the latest articles by review date, then reorder them because the query was wrong. Unless you're using review_date desc for another reason, use last instead.

Related

next, previous to any given object in sorted by multiple columns ActiveRecord set

I got two related models
class Post
belongs_to :subject
end
I need to get posts ordered by 'subjects'.'name' ASC, 'posts'.'updated_at' DESC
Then for any given id to get posts next and previous to Post.find(id) according to given sort order.
I order posts such way:
Post.joins(:subject).order('subjects.name ASC, posts.updated_at DESC')
But how to get next and previous posts ?
You can use each_with_index method. The method works like this:
a=[1,2,3]
a.each_with_index {|item, index|
puts index.to_s+"=>"+item.to_s
}
produces:
0=>1
1=>2
2=>3
Now You can store your sorted array in a variable like:
#posts=Post.joins(:subject).order('subjects.name ASC, posts.updated_at DESC')
And, if you want the next and previous of an id, you can do:
#next=nil
#previous=nil
#posts.each_with_index {|post, index|
if post.id==id
#next=#posts[index+1]
#previous=#posts[index-1]
break
end
}

Sort posts by number of votes within certain time frame

I currently have a voting system implemented in my app and I'm sorting the posts by number of votes with this code in my view:
<%= render #posts.sort_by { |post| post.votes.count }.reverse %>
I want to sort by number of votes for each post by also don't want the post to be any more than lets say 5 days old. How can I sort the posts by both number of votes and date simultaneously.
This is wrong. You should do all sorting operation on the database side.
For this example consider using Arel for creating complex queries or consider create counter cache column.
You could just add a scope to your posts model, something like:
scope :five_days_ago, lambda { where("created_at >= :date", :date => 5.days.ago) }
Then just adjust your render method to the following:
<%= render #posts.five_days_ago.sort_by { |post| post.votes.count }.reverse %>
This assumes you want to keep the structure you are using. Obviously, as other suggested, doing it all in the database is the best course of action.
luacassus is right. It's better do delegate the sorting to the database for at least two reasons:
Performance
You can chain more query methods onto it (necessary for pagination, for example).
The counter cache is probably the best idea, but for the complex query, let me give it a shot. In your Post model:
class << self
def votes_descending
select('posts.*, count(votes.id) as vote_count').joins('LEFT OUTER JOIN votes on votes.post_id = posts.id').group_by('posts.id').order('votes_count desc')
end
def since(date)
where('created_at >= ?', date)
end
end
So...
#posts = Post.votes_descending.since(5.days.ago)
Indeed it will be better to let the db do the sorting. I would do something like
class Post < ActiveRecord::Base
default_scope :order => 'created_at DESC'
end
then you will always have your posts sorted and if I'm not mistaken the last one should be the first you get, so that substitutes your 'reverse' call. Then you can use the scope posted above to get only 5 days old posts. Also check that there is an index on the created_at column in the db. You can do this with
SHOW INDEX FROM posts
The db will do this much faster than ruby.
I figured out another way to do it although I appreciate your help it may not be the cleanest way but I did
def most
range = "created_at #{(7.days.ago.utc...Time.now.utc).to_s(:db)}"
#posts = Post.all(:conditions => range)
#title = "All Posts"
#vote = Vote.new(params[:vote])
respond_to do |format|
format.html
format.json { render :json => #users }
end
end
for my controller
created a route of /most :to => 'posts#most'
and made a view with the original code I had in my view.
I know its not the best way but I am still new to coding so its the best way I could figure out how.

MetaSearch sort ordered column

I have a model:
class Event < ActiveRecord::Base
default_scope :order => 'date_begin'
end
There is a sort link in a view file:
= sort_link #search, :date_begin
When I'm trying to order date_begin as DESC nothing happens because the SQL query is:
SELECT * FROM events ORDER BY date_begin, date_begin DESC
How to make MetaSearch reorder this column? (I know that there is a "reorder" method in ActiveRecord but I don't know how to apply it to MetaSearch)
You can use unscoped method when you decided to use meta_search:
#search = Event.unscoped.search(params[:search])
I also wanted to use a default sort order, and didn't figure out any other way than to enforce a default order in the controller, not using any ordering scope in the model:
search = {"meta_sort" => "created_at.desc"}.merge(params[:search] || {})
#search = Photo.search(search)
The default sort order is created_at DESC, but it will be overwritten if a new sort order is received in the params. Seems to work for me.
#search = if params[:q] && params[:q][:s]
# Ransack sorting is applied - cancel default sorting
Event.reorder(nil).search(params[:q])
else
# Use default sorting
Event.search(params[:q])
end
Benefits:
1) only cancels :order scope - useful if you have .where(:deleted_at => nil).order(:date_begin) default scope.
2) uses default ordering when Ransack sorting is not applied.

Activerecord opitimization - best way to query all at once?

I am trying to achieve by reducing the numbers of queries using ActiveRecord 3.0.9. I generated about 'dummy' 200K customers and 500K orders.
Here's Models:
class Customer < ActiveRecord::Base
has_many :orders
end
class Orders < ActiveRecord::Base
belongs_to :customer
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :order
end
when you are using this code in the controller:
#customers = Customer.where(:active => true).paginate(page => params[:page], :per_page => 100)
# SELECT * FROM customers ...
and use this in the view (I removed HAML codes for easier to read):
#order = #customers.each do |customer|
customer.orders.each do |order| # SELECT * FROM orders ...
%td= order.products.count # SELECT COUNT(*) FROM products ...
%td= order.products.sum(:amount) # SELECT SUM(*) FROM products ...
end
end
However, the page is rendered the table with 100 rows per page. The problem is that it kinda slow to load because its firing about 3-5 queries per customer's orders. thats about 300 queries to load the page.
There's alternative way to reduce the number of queries and load the page faster?
Notes:
1) I have attempted to use the includes(:orders), but it included more than 200,000 order_ids. that's issue.
2) they are already indexed.
If you're only using COUNT and SUM(amount) then what you really need is to retrieve only that information and not the orders themselves. This is easily done with SQL:
SELECT customer_id, order_id, COUNT(id) AS order_count, SUM(amount) AS order_total FROM orders LEFT JOIN products ON orders.id=products.order_id GROUP BY orders.customer_id, products.order_id
You can wrap this in a method that returns a nice, orderly hash by re-mapping the SQL results into a structure that fits your requirements:
class Order < ActiveRecord::Base
def self.totals
query = "..." # Query from above
result = { }
self.connection.select_rows(query).each do |row|
# Build out an array for each unique customer_id in the results
customer_set = result[row[0].to_i] ||= [ ]
# Add a hash representing each order to this customer order set
customer_set << { :order_id => row[1].to_i, :count => row[2].to_i, :total => row[3].to_i } ]
end
result
end
end
This means you can fetch all order counts and totals in a single pass. If you have an index on customer_id, which is imperative in this case, then the query will usually be really fast even for large numbers of rows.
You can save the results of this method into a variable such as #order_totals and reference it when rendering your table:
- #order = #customers.each do |customer|
- #order_totals[customer.id].each do |order|
%td= order[:count]
%td= order[:total]
You can try something like this (yes, it looks ugly, but you want performance):
orders = Order.find_by_sql([<<-EOD, customer.id])
SELECT os.id, os.name, COUNT(ps.amount) AS count, SUM(ps.amount) AS amount
FROM orders os
JOIN products ps ON ps.order_id = os.id
WHERE os.customer_id = ? GROUP BY os.id, os.name
EOD
%td= orders.name
%td= orders.count
%td= orders.amount
Added: Probably it is better to create count and amount cache in Orders, but you will have to maintain it (count can be counter-cache, but I doubt there is a ready recipe for amount).
You can join the tables in with Arel (I prefer to avoid writing raw sql when possible). I believe that for your example you would do something like:
Customer.joins(:orders -> products).select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
The first method--
Customer.joins(:orders -> products)
--pulls in the nested association in one statement. Then the second part--
.select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
--specifies exactly what columns you want back.
Chain those and I believe you'll get a list of Customer instances only populated with what you've specified in the select method. You have to be careful though because you now have in hand read only objects that are possibly in in invalid state.
As with all the Arel methods what you get from those methods is an ActiveRecord::Relation instance. It's only when you start to access that data that it goes out and executes the SQL.
I have some basic nervousness that my syntax is incorrect but I'm confident that this can be done w/o relying on executing raw SQL.

ActiveRecord and SELECT AS SQL statements

I am developing in Rails an app where I would like to rank a list of users based on their current points. The table looks like this: user_id:string, points:integer.
Since I can't figure out how to do this "The Rails Way", I've written the following SQL code:
self.find_by_sql ['SELECT t1.user_id, t1.points, COUNT(t2.points) as user_rank FROM registrations as t1, registrations as t2 WHERE t1.points <= t2.points OR (t1.points = t2.points AND t1.user_id = t2.user_id) GROUP BY t1.user_id, t1.points ORDER BY t1.points DESC, t1.user_id DESC']
The thing is this: the only way to access the aliased column "user_rank" is by doing ranking[0].user_rank, which brinks me lots of headaches if I wanted to easily display the resulting table.
Is there a better option?
how about:
#ranked_users = User.all :order => 'users.points'
then in your view you can say
<% #ranked_users.each_with_index do |user, index| %>
<%= "User ##{index}, #{user.name} with #{user.points} points %>
<% end %>
if for some reason you need to keep that numeric index in the database, you'll need to add an after_save callback to update the full list of users whenever the # of points anyone has changes. You might look into using the acts_as_list plugin to help out with that, or that might be total overkill.
Try adding user_rank to your model.
class User < ActiveRecord::Base
def rank
#determine rank based on self.points (switch statement returning a rank name?)
end
end
Then you can access it with #user.rank.
What if you did:
SELECT t1.user_id, COUNT(t1.points)
FROM registrations t1
GROUP BY t1.user_id
ORDER BY COUNT(t1.points) DESC
If you want to get all rails-y, then do
cool_users = self.find_by_sql ['(sql above)']
cool_users.each do |cool_user|
puts "#{cool_user[0]} scores #{cool_user[1]}"
end

Resources