ActiveRecord Select with Parameter Binding - ruby-on-rails

I want to use a subquery with ActiveRecord like so:
User.select(
'users.*,
(select sum(amount)
from subscriptions
where subscriptions.user_id = user.id
and created_at < ? ) as amt)'
).order('amt')
However, on the second to last line, I have the problem that I can't figure out how to bind the Time class parameter, as ActiveRecord::Base's select method doesn't accept more than one parameter (the sql string). What do I do?

You can use one of the ActiveRecord::Sanitization class methods.
These are mixed into ActiveRecord::Base and are almost all protected so they are most easily accessed by defining a class method on your model:
class User < ActiveRecord::Base
def self.select_with_args(sql, args)
query = sanitize_sql_array([sql, args].flatten)
select(query)
end
end
It's also worth looking for other ways to fetch your data and comparing their performance to the subquery method. For example, you could fetch the subscription counts with a separate query:
subs_by_user = Subscription.group(:user_id).where('created_at < ?', d).count()

Related

Query records by class method

If I have a class method for a product returning its average rating, how do I query products with average_rating greater than a param I receive from frontend?
class Product < ApplicationRecord
def average_rating
ratings.sum(:rate) / ratings.count
end
end
For a scope I would need to pass the param from controller to model. Should I do that? Or should I just calculate the average rating in the controller instead? Or save average_rating in database with a callback when a rating is saved?
You cannot call class methods from SQL. You can however load all records into memory and do this, which is quite inefficient if you have a lot of records.
Product.all.select { |record| average_rating > record.value }
If this is a ruby method you can't do it (with SQL), unless you move your "average" logic into SQL too.
If the amount of records is a small one of the simplest solution is to use Ruby.
Product.all.select{|e| :&average_rating > e.value}

Filter ActiveRecord objects based on model method?

Is there a way to use a filter criterion in where, which is not a DB column. If I have a Movie model with the following method:
def blockbuster?
imdb_rating > 8
end
is there a way to do something like Movie.where(:blockbuster? => true). I know that in this particular example it's possible to just use the imdb_rating attribute (Movie.where('imdb_rating > ?', 8)), but there are cases, when a method does a more complex logic. Currently, if I want to do this, I must call Movie.all.select(&:blockbuster?), but I want to do this at the DB level. Thank you.
P.S. I am sure that similar questions are asked many times, but I can't seem to think of the right keywords to find them here or on Google. Please, post a link if this is answered elsewhere.
Have you tried making it into a scope? There is some information on scopes in the Rails documentation.
Basically, with your method, you'd do something like:
class Movie < ActiveRecord::Base
scope :blockbuster, -> { where('imdb_rating > ?', 8) }
end
Movie.blockbuster # returns all relevant objects as an ActiveRecord relation
Class methods are also available as scopes:
class Movie < ActiveRecord::Base
def self.blockbuster?
where('imdb_rating ?', 8)
end
end
Movie.blockbuster? # also returns all relevant objects as an ActiveRecord relation

ORDER BY and DISTINCT ON (...) in Rails

I am trying to ORDER by created_at and then get a DISTINCT set based on a foreign key.
The other part is to somehow use this is ActiveModelSerializer. Specifically I want to be able to declare:
has_many :somethings
In the serializer. Let me explain further...
I am able to get the results I need with this custom sql:
def latest_product_levels
sql = "SELECT DISTINCT ON (product_id) client_product_levels.product_id,
client_product_levels.* FROM client_product_levels WHERE client_product_levels.client_id = #{id} ORDER BY product_id,
client_product_levels.created_at DESC";
results = ActiveRecord::Base.connection.execute(sql)
end
Is there any possible way to get this result but as a condition on a has_many relationship so that I can use it in AMS?
In pseudo code: #client.products_levels
Would do something like: #client.order(created_at: :desc).select(:product_id).distinct
That of course fails for reasons that are beyond me.
Any help would be great.
Thank you.
A good way to structure this is to split your query into two parts: the first part manages the filtering of rows so that you get only your latest client product levels. The second part uses a standard has_many association to connect Client with ClientProductLevel.
Starting with your ClientProductLevel model, you can create a scope to do the latest filtering:
class ClientProductLevel < ActiveRecord::Base
scope :latest, -> {
select("distinct on(product_id) client_product_levels.product_id,
client_product_levels.*").
order("product_id, created_at desc")
}
end
You can use this scope anywhere that you have a query that returns a list of ClientProductLevel objects, e.g., ClientProductLevel.latest or ClientProductLevel.where("created_at < ?", 1.week.ago).latest, etc.
If you haven't already done so, set up your Client class with a has_many relationship:
class Client < ActiveRecord::Base
has_many :client_product_levels
end
Then in your ActiveModelSerializer try this:
class ClientSerializer < ActiveModel::Serializer
has_many :client_product_levels
def client_product_levels
object.client_product_levels.latest
end
end
When you invoke the ClientSerializer to serialize a Client object, the serializer sees the has_many declaration, which it would ordinarily forward to your Client object, but since we've got a locally defined method by that name, it invokes that method instead. (Note that this has_many declaration is not the same as an ActiveRecord has_many, which specifies a relationship between tables: in this case, it's just saying that the serializer should present an array of serialized objects under the key `client_product_levels'.)
The ClientSerializer#client_product_levels method in turn invokes the has_many association from the client object, and then applies the latest scope to it. The most powerful thing about ActiveRecord is the way it allows you to chain together disparate components into a single query. Here, the has_many generates the `where client_id = $X' portion, and the scope generates the rest of the query. Et voila!
In terms of simplification: ActiveRecord doesn't have native support for distinct on, so you're stuck with that part of the custom sql. I don't know whether you need to include client_product_levels.product_id explicitly in your select clause, as it's already being included by the *. You might try dumping it.

How to order query results based on date field in parent model in rails4/activerecord?

I have models
class Run < ActiveRecord::Base
belongs_to :race
end
class Race < ActiveRecord::Base
has_many :runs
end
Race has a date field and run has a name field.
I would like a query to present all runs from specific name ordered by date field in the race.
I have tried following, but it doesn't sort correctly, results are presented randomly.
Run.where(:name => 'foo').joins(:race).order('races.date ASC')
Thinking that problem might be in the date field I also tried sorting based on id in Race, but results were incorrect as well. I also tried placing join as first element after Run, but no change in results.
SQL generated is as follows:
"SELECT `runs`.* FROM `runs` INNER JOIN `races` ON `races`.`id` = `runs`.`race_id` WHERE (name = 'foo') ORDER BY odds ASC, races.date ASC"
Any help appreciated.
Since you're using default_scope for your model, Active Record always uses your order method by 'odds' first. Do not use default_scope unless you really have to. You'll probably be better off with regular scopes(look at section 14).
You can use unscoped also, which makes AR "forget" the default_scopes. Use it like:
Run.unscoped.where(:name => 'foo').joins(:race).order('races.date ASC')
There is also the reorder method that also overrides the default scope order method:
Run.where(:name => 'foo').joins(:race).reorder('races.date ASC')

Join counts across tables

I have three objects (List, ListType and GlobalList) and am trying to get a count of the items in GlobalList with a specific global_id and a specific list_type_id.
This seems to work:
select count(*) from globals_lists gl, lists l where gl.global_id=946 and gl.list_id=l.id and l.list_type_id=10;
The structure is the following:
class ListType < ActiveRecord::Base
has_many: lists
end
class List < ActiveRecord::Base
has_many :global_lists
belongs_to :list_type
end
class GlobalList < ActiveRecord::Base
belongs_to :list
end
Not sure how to do AR call - at this but can seem to put a where on the join. The list_type_id will always be 10.
a=GlobalList.where("global_id=?",119).joins(:list)
How would I do this ActiveRecord call? Most of the stuff I found via Google involved named scopes, but it seems like this should be able to be done without scopes.
First approach:
result = GlobalList
.select("COUNT(*) AS total")
.where("globals_lists.global_id=? AND lists.list_type_id=?",119, 10)
.joins(:list)
Then you will account your result using result.total
Second approach is tho use count method instead of select("COUNT(*) AS total")
GlobalList
.where("globals_lists.global_id=? AND lists.list_type_id=?",119, 10)
.joins(:list)
.count
You can find more about ActiveRecord Query Interface in Rails Guides, or directly at what you are interested in
count and joins

Resources