Rails 3 Applying limit and offset to subquery - sql

I have a query that goes something like this (in song.rb):
def self.new_songs
Song.where(id: Song.grouped_order_published).select_important_stuff
end
Later on in my app, it is then passed the limit and offset, lets say in the controller:
#songs = Song.new_songs.limit(10).offset(10)
The way my app is structured, I'd like to keep this method of setting things, but unfortunately it is really slow as it is limiting the outer query rather than the subquery.
Is there a way I can expose the subquery such that it receives the limit and offset rather than the outer query?
Edit: I should add I am using postgres 9.2.
Edit 2: The reason why I want to do it in this fashion is I am doing pagination and I need to get the "count" of the total number of rows. So I do something like this:
#songs = Song.new_songs
...
#pages = #songs.count / 10
...
render #songs.limit(params[:page]).offset(0)
If I were to change it somehow, I'd have to redo this entirely (which is in a ton of places). By not limiting it until it's actually called, I can do the count in between and then get just the page at the end. I guess I'm looking more for advice on how this can be done with the inner query, without becoming horribly slow as the database grows.

I could not try the solution and I am not a ruby expert either, but as far as I understand the problem you would need an object that passes all method-calls but limit and offset onto the full query and store the limited sub_query in the meantime.
It could probably look like this:
class LimitedSubquery < Object
# sub_query has to be stored so we can limit/offset it
def initialize(sub_query)
#sub_query = sub_query
end
# Make sure everybody knows we can be used like a query
def self.respond_to?(symbol, include_private=false)
super || full_query.respond_to?(symbol, include_private)
end
# Missing methods are probably meant to be called on the whole query
def self.method_missing(method_sym, *arguments, &block)
if full_query.respond_to?(method_sym)
full_query.send(method_sym, *arguments, &block)
else
super
end
end
# Generate the query for execution
def self.full_query
Song.where(id: #sub_query).select_important_stuff
end
# Apply limit to sub_query
def self.limit(*number)
LimitedSubquery.new(#sub_query.limit(*number))
end
# Apply offset to sub_query
def self.offset(*number)
LimitedSubquery.new(#sub_query.offset(*number))
end
end
And than call it like
def new_songs
LimitedSubquery.new(Song.grouped_order_published)
end
Please edit me if I got something wrong!
Regards
TC

You should consider using the will_paginate gem. This keeps you away form the hazzle to calculate all this by hand ;-)

Related

How to send a table names as parameters to a function which performs a join on them?

Currently I used the following code for joining tables.
Booking.joins(:table1, :table2, :table3, :table4).other_queries
However, the number of tables to be joined with depends on certain conditions. The other_queries also form a very large chain. So, I am duplicating a lot of code just because I need to perform joins differently.
So, I want to implement something like this
def method(params)
Booking.joins(params).other_queries
end
How can this be done?
Maybe just Booking.joins(*params).other_queries is what you need?
Operator * transforms array into list of params, for example:
arr = [1,2,3]
any_method(*arr) # is equal to any_method(1,2,3)
However, if params is smth came from user I recommend you not to trust it, it probably could be security issue. But if you trust it or filter it - why not.
SAFE_JOINS = [:table1, :table2, :table3]
def method(params)
booking = Booking.scoped # or Booking.all if you are rails 5
(params[:joins] & SAFE_JOINS.map(&:to_s)).each do |j|
booking = booking.joins(j.intern)
end
end

Rails Order by frequency of a column in another table

I have a table KmRelationship which associates Keywords and Movies
In keyword index I would like to list all keywords that appear most frequently in the KmRelationships table and only take(20)
.order doesn't seem to work no matter how I use it and where I put it and same for sort_by
It sounds relatively straight forward but i just can't seem to get it to work
Any ideas?
Assuming your KmRelationship table has keyword_id:
top_keywords = KmRelationship.select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20)
This may not look right in your console output, but that's because rails doesn't build out an object attribute for the calculated frequency column.
You can see the results like this:
top_keywords.each {|k| puts "#{k.keyword_id} : #{k.freqency}" }
To put this to good use, you can then map out your actual Keyword objects:
class Keyword < ActiveRecord::Base
# other stuff
def self.most_popular
KmRelationship.
select('keyword_id, count(keyword_id) as frequency').
order('frequency desc').
group('keyword_id').
take(20).
map(&:keyword)
end
end
And call with:
Keyword.most_popular
#posts = Post.select([:id, :title]).order("created_at desc").limit(6)
I have this listed in my controller index method which allows the the order to show the last post with a limit of 6. It might be something similar to what you are trying to do. This code actually reflects a most recent post on my home page.

Rails order active record results based on one column and another if null

So I have an active record query that returns some records, lets say it looks like this.
jobs = Job.where(:user_id => current_user.id)
As you would expect this returns the current_user's jobs. Assume that the job has two dates, deadline_date and due_date. If I want to order on deadline_date I can do something like..
jobs.order("deadline_date asc")
That works as expected, now image I have something in the job model like this.
class Job < ActiveRecord::Base
def deadline_date
self.read_attribute(:deadline_date) || self.due_date
end
end
So the job will display its deadline_date if it is not nil else it will fallback to use the due_date. So to sort this I have done the following...
jobs.sort_by{|job| job.deadline_date}
jobs.sort_by{|job| job.deadline_date}.reverse
This solves my problem but I wondered it there were better alternatives, is it possible to achieve this using SQL? Also this produces some repeated code in that I have a sort_order variable in my controllers that I can pass directly like this...
jobs.order(sort_order)
Now it looks more like this...
if params[:sort] == "deadline_date"
if params[:order] == "asc"
jobs.sort_by{|job| job.deadline_date}
else
jobs.sort_by{|job| job.deadline_date}.reverse
end
else
jobs.order(sort_order)
end
Note: This is a arbitrary example in reality it is a bit messy but you get the idea. So I'm looking for an SQL alternative or a suggestion on how it could be improved. Cheers
If I understand you correctly, you should be able to do this using the COALESCE SQL function:
jobs.order(Arel.sql("COALESCE(deadline_date, due_date)"))
That's pretty much the same as deadline_date || due_date in Ruby.
I believe it to be a standard SQL thing, so it should work for most SQL dialects.

Update more record in one query with Active Record in Rails

Is there a better way to update more record in one query with different values in Ruby on Rails? I solved using CASE in SQL, but is there any Active Record solution for that?
Basically I save a new sort order when a new list arrive back from a jquery ajax post.
#List of product ids in sorted order. Get from jqueryui sortable plugin.
#product_ids = [3,1,2,4,7,6,5]
# Simple solution which generate a loads of queries. Working but slow.
#product_ids.each_with_index do |id, index|
# Product.where(id: id).update_all(sort_order: index+1)
#end
##CASE syntax example:
##Product.where(id: product_ids).update_all("sort_order = CASE id WHEN 539 THEN 1 WHEN 540 THEN 2 WHEN 542 THEN 3 END")
case_string = "sort_order = CASE id "
product_ids.each_with_index do |id, index|
case_string += "WHEN #{id} THEN #{index+1} "
end
case_string += "END"
Product.where(id: product_ids).update_all(case_string)
This solution works fast and only one query, but I create a query string like in php. :) What would be your suggestion?
You should check out the acts_as_list gem. It does everything you need and it uses 1-3 queries behind the scenes. Its a perfect match to use with jquery sortable plugin. It relies on incrementing/decrementing the position (sort_order) field directly in SQL.
This won't be a good solution for you, if your UI/UX relies on saving the order manually by the user (user sorts out the things and then clicks update/save). However I strongly discourage this kind of interface, unless there is a specific reason (for example you cannot have intermediate state in database between old and new order, because something else depends on that order).
If thats not the case, then by all means just do an asynchronous update after user moves one element (and acts_as_list will be great to help you accomplish that).
Check out:
https://github.com/swanandp/acts_as_list/blob/master/lib/acts_as_list/active_record/acts/list.rb#L324
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_class.unscoped.where(
"#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
).update_all(
"#{position_column} = (#{position_column} + 1)"
)
end

Possible to use ActiveRecord methods against AR collections?

I would like to be able to pull all records from the db:
u = User.all
And then once loaded be able to apply AR methods to the resulting collection:
u.first
Is this possible in rails?
Once you actually query the database, the results become an array instead of an ActiveRecord::Relation. (Though #first would still work fine, since it's a method that also exists on Array).
If you just need a starting point to build an ActiveRecord::Relation though, you can use scoped:
# Doesn't execute a query yet
u = User.scoped
# This now executes a query similar to SELECT * FROM users LIMIT 1
u.first
Note that in Rails 4.0, #all now does the same thing as #scoped (whereas in Rails 3, it returns an array).
Why don't you try it?
User.all doesn't return an AR collection it returns an Array. Get rid of the .all and you will have a working example.