How to create Rails query that (I think) requires PostgreSQL subquery? - ruby-on-rails

Not sure how to do this so title may not be correct.
Each User has a field country of type String.
Given an array of user_id, country tuples for the query, find all the records that match. Each User must be found with it's own country.
For example, here is the array of tuples.
[1, 'us'],
[2, 'mexico'],
[3, 'us']
This would return User 1 if it exists and its country is 'us'.
It should also return User 2 if it exists and its country is 'mexico'.
The query should return all matching results.
Rails 4.2

class User < ApplicationRecord
def self.query_from_tuples(array_of_tuples)
array_of_tuples.inject(nil) do |scope, (id, country)|
if scope
scope.or(where(id: id, country: country))
else
where(id: id, country: country) # this handles the initial iteration
end
end
end
end
The resulting query is:
SELECT "users".* FROM "users"
WHERE (("users"."id" = $1 AND "users"."country" = $2 OR "users"."id" = $3 AND "users"."country" = $4) OR "users"."id" = $5 AND "users"."country" = $6)
LIMIT $7
You could also adapt kamakazis WHERE (columns) IN (values) query by:
class User < ApplicationRecord
def self.query_from_tuples_2(array_of_tuples)
# just a string of (?,?) SQL placeholders for the tuple values
placeholders = Array.new(array_of_tuples.length, '(?,?)').join(',')
# * is the splat operator and turns the tuples (flattened) into
# a list of arguments used to fill the placeholders
self.where("(id, country) IN (#{placeholders})", *array_of_tuples.flatten)
end
end
Which results in the following query which is a lot less verbose:
SELECT "users".* FROM "users"
WHERE ((id, country) IN ((1,'us'),(2,'mexico'),(3,'us'))) LIMIT $1
And can also perform much better if you have a compound index on [id, country].

I know this would work in pure SQL: e.g.
SELECT * FROM user
WHERE (id, country) IN ((1, 'us'), (2, 'mexico'), (3, 'us'))
Now I don't know how Rails would handle the bind parameter if it was a list of pairs (list of two elements each). Perhaps that would work.

You can construct a raw sql and use active record. Something like this:
def self.for_multiple_lp(arr=[])
# Handle case when arr is not in the expected format.
condition = arr.collect{|a| "(user_id = #{a[0]} AND country = #{a[1]})"}.join(" OR ")
where(condition)
end
Edit: Improved Solution
def self.for_multiple_lp(arr=[])
# Handle case when arr is not in the expected format.
condition = arr.collect{|a| "(user_id = ? AND country = ?)"}.join(" OR ")
where(condition, *(arr.flatten))
end
This should work.

Related

How to pluck a column value in has one association?

I have a Vehicle model which has a has_one association with QrCode.
I want to pluck a specific column of qr_code rather than selecting all the columns and mapping single value
I have tried the following code.
vehicle = Vehicle.first
code = vehicle.qr_code.pluck(:value)
But this is not a valid query
Following code will have the desired value.
code = vehicle.qr_code.value
But the query build by this code is
SELECT "qr_codes".* FROM "qr_codes" WHERE "qr_codes"."codeable_id" = $1 AND "qr_codes"."codeable_type" = $2 LIMIT 1 [["codeable_id", 1], ["codeable_type", "Vehicle"]]
This is expensive as it selects all column values and there are few columns in qr_codes table that store huge data.
Following is the code implementation
class Vehicle < ActiveRecord::Base
has_one :qr_code, as: :codeable
end
class QrCode < ActiveRecord::Base
belongs_to :codeable, polymorphic: true
end
not expected query:
SELECT "qr_codes".* FROM "qr_codes" WHERE "qr_codes"."codeable_id" = $1 AND "qr_codes"."codeable_type" = $2 LIMIT 1 [["codeable_id", 1], ["codeable_type", "Vehicle"]]
expected query:
SELECT "qr_codes".value FROM "qr_codes" WHERE "qr_codes"."codeable_id" = $1 AND "qr_codes"."codeable_type" = $2 LIMIT 1 [["codeable_id", 1], ["codeable_type", "Vehicle"]]
You can use query like below for getting vehicle first record with its qr code's value name
Vehicle
.left_outer_joins(:qr_code)
.select("vehicles.*,
(SELECT qr_codes.value from qr_codes WHERE vehicles.id = qr_codes.codeable_id) as value_name")
.limit(1)
When you have Vehicle instance:
QrCode.
where(codeable: vehicle).
pluck(:value).
first
When you have vehicle_id only:
Vehicle.
left_joins(:qr_code).
where(id: vehicle_id).
pluck('qr_codes.value').
first
As per your expected query -
QrCode.where(codeable: Vehicle.first).pluck(:value).first
This will select only value column in the query.
Vehicle.joins(:qr_code).select("qr_codes.value").pluck(:value)
For a specific vehicle:
Vehicle.where(id: 1).joins(:qr_code).select("qr_codes.value").pluck(:value)

Ruby/Rails - Chain unknown number of method calls

I would like to dynamically create (potentially complex) Active Record queries from a 2D array passed into a method as an argument. In other words, I'd like to take this:
arr = [
['join', :comments],
['where', :author => 'Bob']
]
And create the equivalent of this:
Articles.join(:comments).where(:author => 'Bob')
One way to do this is:
Articles.send(*arr[0]).send(*arr[1])
But what if arr contains 3 nested arrays, or 4, or 5? A very unrefined way would be to do this:
case arr.length
when 1
Articles.send(*arr[0])
when 2
Articles.send(*arr[0]).send(*arr[1])
when 3
Articles.send(*arr[0]).send(*arr[1]).send(*arr[2])
# etc.
end
But is there a cleaner, more succinct way (without having to hit the database multiple times)? Perhaps some way to construct a chain of method calls before executing them?
One convenient way would be to use a hash instead of a 2D array.
Something like this
query = {
join: [:comments],
where: {:author => 'Bob'}
}
This approach is not much complex and You don't need to worry if the key is not provided or is empty
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob'"
If the keys are empty or not present at all
query = {
join: []
}
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles`"
Or nested
query = {
join: [:comments],
where: {:author => 'Bob', comments: {author: 'Joe'}}
}
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob' AND `comments`.`author` = 'Joe'"
I created following query which will work on any model and associated chained query array.
def chain_queries_on(klass, arr)
arr.inject(klass) do |relation, query|
begin
relation.send(query[0], *query[1..-1])
rescue
break;
end
end
end
I tested in local for following test,
arr = [['where', {id: [1,2]}], ['where', {first_name: 'Shobiz'}]]
chain_queries_on(Article, arr)
Query fired is like below to return proper output,
Article Load (0.9ms) SELECT `article`.* FROM `article` WHERE `article`.`id` IN (1, 2) AND `article`.`first_name` = 'Shobiz' ORDER BY created_at desc
Note-1: few noticeable cases
for empty arr, it will return class we passed as first argument in method.
It will return nil in case of error. Error can occur if we use pluck which will return array (output which is not chain-able) or if we do not pass class as first parameter etc.
More modification can be done for improvement in above & avoid edge cases.
Note-2: improvements
You can define this method as a class method for Object class also with one argument (i.e. array) and call directly on class like,
# renamed to make concise
Article.chain_queries(arr)
User.chain_queries(arr)
Inside method, use self instead of klass
arr.inject(Articles){|articles, args| articles.send(*args)}

Random ActiveRecord with where and excluding certain records

I would like to write a class function for my model that returns one random record that meets my condition and excludes some records. The idea is that I will make a "random articles section."
I would like my function to look like this
Article.randomArticle([1, 5, 10]) # array of article ids to exclude
Some pseudo code:
ids_to_exclude = [1,2,3]
loop do
returned_article = Article.where(published: true).sample
break unless ids_to_exclude.include?(returned_article.id)
do
Lets look at DB specific option.
class Article
# ...
def self.random(limit: 10)
scope = Article.where(published: true)
# postgres, sqlite
scope.limit(limit).order('RANDOM()')
# mysql
scope.limit(limit).order('RAND()')
end
end
Article.random asks the database to get 10 random records for us.
So lets look at how we would add an option to exclude some records:
class Article
# ...
def self.random(limit: 10, except: nil)
scope = Article.where(published: true)
if except
scope = scope.where.not(id: except)
end
scope.limit(limit).order('RANDOM()')
end
end
Now Article.random(except: [1,2,3]) would get 10 records where the id is not [1,2,3].
This is because .where in rails returns a scope which is chain-able. For example:
> User.where(email: 'test#example.com').where.not(id: 1)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 AND ("users"."id" != $2) [["email", "test#example.com"], ["id", 1]]
=> #<ActiveRecord::Relation []>
We could even pass a scope here:
# cause everyone hates Bob
Article.random( except: Article.where(author: 'Bob') )
See Rails Quick Tips - Random Records for why a DB specific solution is a good choice here.
You can use some like this:
ids_to_exclude = [1,2,3,4]
Article.where("published = ? AND id NOT IN (?)", true , ids_to_exclude ).order( "RANDOM()" ).first

How to query by ActiveRecord depending If the value of an attribute is empty or not?

I have a variable that could be set or left empty. In the first case the query looks fine, but in the second one I can't find how it works.
Checking If :
if params['customer_id'] == ""
#customer_id = "";
else
#customer_id = params['customer_id']
end
The query
User.where("customer_id = ?", #customer_id)
The problem is that If "" ,the query returns nothing. I could write it as
if params['customer_id'] == ""
User.all
else
User.where("customer_id = ?", params['customer_id'])
end
but first this is not DRY and second my query will include 10 * where's so this is not a very smart way to accomplish it.
You will refactor your query as:
#users = User.all
#users = #users.where(customer_id: params['customer_id']) if params['customer_id'].present?
Example:
#users = User.all
# User Load (6.8ms) SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL
#users.where(email: 'arup').count
# (1.2ms) SELECT COUNT(*) FROM "users" WHERE "users"."deleted_at" IS NULL AND "users"."email" = $1 [["email", "arup"]]
update
with scope
scope :with_or_without_customer, ->(customer_id) do
customer_id.present? ? where(customer_id: customer_id) : all
end
Note:
Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array.
In some specific cases, this may cause breakage when upgrading. However in most cases the ActiveRecord::Relation will just act as a lazy-loaded array and there will be no problems.
You can try this way.
#condition = "1"
#condition = {:customer_id => params['customer_id']} if params['customer_id'].present?
You can create your condition first then fire the query on database so that it will fire query only single time on database:
#users = User.where(#condition)

How to eager load child model's sum value for ruby on rails?

I have an Order model, it has many items, it looks like this
class Order < ActiveRecord::Base
has_many :items
def total
items.sum('price * quantity')
end
end
And I have an order index view, querying order table like this
def index
#orders = Order.includes(:items)
end
Then, in the view, I access total of order, as a result, you will see tons of SUM query like this
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 1]]
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 2]]
SELECT SUM(price * quantity) FROM "items" WHERE "items"."order_id" = $1 [["order_id", 3]]
...
It's pretty slow to load order.total one by one, I wonder how can I load the sum in a eager manner via single query, but still I can access order.total just like before.
Try this:
subquery = Order.joins(:items).select('orders.id, sum(items.price * items.quantity) AS total').group('orders.id')
#orders = Order.includes(:items).joins("INNER JOIN (#{subquery.to_sql}) totals ON totals.id = orders.id")
This will create a subquery that sums the total of the orders, and then you join that subquery to your other query.
I wrote up two options for this in this blog post on using find_by_sql or joins to solve this.
For your example above, using find_by_sql you could write something like this:
Order.find_by_sql("select
orders.id,
SUM(items.price * items.quantity) as total
from orders
join items
on orders.id = items.order_id
group by
order.id")
Using joins, you could rewrite as:
Order.all.select("order.id, SUM(items.price * items.quantity) as total").joins(:items).group("order.id")
Include all the fields you want in your select list in both the select clause and the group by clause. Hope that helps!

Resources