How to pluck a column value in has one association? - ruby-on-rails

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)

Related

How to create Rails query that (I think) requires PostgreSQL subquery?

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.

StatementInvalid Rails Query

I've got the following query that works:
jobs = current_location.jobs.includes(:customer).all.where(complete: complete)
However, when I add a where clause to query the first name of the customer table, I get an error.
jobs = current_location.jobs.includes(:customer).all.where(complete: complete).where("customers.fist_name = ?", "Bob")
Here is the error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "customers"
LINE 1: ...bs"."complete" = $2 AND "jobs"."status" = $3 AND (customers....
^
: SELECT "jobs".* FROM "jobs" INNER JOIN "jobs_users" ON "jobs"."id" = "jobs_users"."job_id" WHERE "jobs_users"."user_id" = $1 AND "jobs"."complete" = $2 AND "jobs"."status" = $3 AND (customers.last_name = 'Bob') ORDER BY "jobs"."start" DESC LIMIT $4 OFFSET $5
The current_location method:
def current_location
return current_user.locations.find_by(id: cookies[:current_location])
end
Location Model
has_many :jobs
has_and_belongs_to_many :customers
Job Model
belongs_to :location
belongs_to :customer
Customer Model
has_many :jobs
has_and_belongs_to_many :locations
How can I fix this issue?
includes will only join the table if you set a reference to the association.
When using includes you ensure a reference to the association in 2 fashions:
You can use the references method this will join the table whether or not there are any query conditions (If you MUST use raw SQL as shown in your question then this is the method you would need to use) e.g.
current_location.jobs
.includes(:customer)
.references(:customer)
Or you can use the hash finder version of where: (Please note that when using an associative reference in the where clause you must reference the table name, in this case customers and not the association name customer)
current_location.jobs
.includes(:customer)
.where(customers: {first_name: "Bob" })
Both of these will eager load the customer for the jobs referenced.
The first option (references) will OUTER JOIN the customers table so that all the jobs are loaded even if they have no customers as long as no query conditions reference the customers table.
The second option (using where) will OUTER JOIN the customers table but given the query parameter against the customers table it will act very much like an INNER JOIN.
If you only need to search the jobs based on customer information then joins is a better choice as this will create an INNER JOIN with the customers table but will not try to load any of the customer data in the query e.g.
current_location.jobs.joins(:customer).where(customers: {first_name: "Bob" })
joins will always include the associated table regardless of a reference in the query.
Sidenote: the all in both your queries is completely unnecessary
includes(:customer) does not necessarily join the customers table into the SQL query. You need to use joins(:customer) to force Rails to join the customers table into the SQL query and make it available to query conditions.
jobs = current_location.jobs
.joins(:customer)
.includes(:customer)
.where(complete: complete)
.where(customers: { first_name: 'Bob' })

Get count from another table

I have a many-to-many, HMT model setup and I want to add a count value to return as part of my tab_community#index method. The models for the table of interest and the join table are as follows:
class TabCommunity < ApplicationRecord
belongs_to :ref_community_type
has_many :tab_client_project_communities
has_many :tab_projects, through: :tab_client_project_communities
has_many :tab_community_accounts
has_many :tab_accounts, through: :tab_community_accounts
has_many :tab_client_project_communities
has_many :ref_platforms, through: :tab_client_project_communities
end
class TabCommunityAccount < ApplicationRecord
belongs_to :tab_community
belongs_to :tab_account
end
The #index method currently looks like this:
_tab_community_ids = params[:tab_community_ids].split(',')
#tab_communities = TabCommunity.where(id: _tab_community_ids).includes(:ref_platforms).all.order(:updated_at).reverse_order
This query is what I want to replicate in ActiveRecord:
select (select count(*) from tab_community_accounts where tab_community_id = c.id) as cnt, c.* from tab_communities c
The results I want are below:
7318 149 sports_writers 7 2017-12-17 15:45:36.946965 2017-12-17 15:45:36.946965
0 172 random_admin 8 2018-04-16 19:21:21.844041 2018-04-16 19:21:21.844041
2731 173 random_aacc 7 2018-04-16 19:22:35.074461 2018-04-16 19:22:35.074461
(The 1st column is count(*) from tab_community_accounts, the rest is from tab_communities.)
From what I've seen so far I should use either .select() or .pluck() but neither one works for me. I tried this out:
TabCommunity.pluck("distinct tab_community_accounts.tab_account_id as cnt").where(id: _tab_community_ids).includes(:ref_platforms).all.order(:updated_at).reverse_order
Is this close to what I need or am I completely off?
What you want is something like:
#tab_communities = TabCommunity
.where(id: _tab_community_ids)
.select('tab_communities.*, count(tab_community_accounts.id) AS cnt')
.left_outer_joins(:tab_community_accounts)
.includes(:ref_platforms) # consider if you actually need this
.group(:id)
.order(updated_at: :desc) # use an explicit order instead!
TabCommunity Load (1.1ms) SELECT tab_communities.*, count(tab_community_accounts.id) AS cnt FROM "tab_communities" LEFT OUTER JOIN "tab_community_accounts" ON "tab_community_accounts"."tab_community_id" = "tab_communities"."id" WHERE "tab_communities"."id" = 1 GROUP BY "tab_communities"."id" ORDER BY "tab_communities"."updated_at" DESC
=> #<ActiveRecord::Relation [#<TabCommunity id: 1, created_at: "2018-05-07 21:13:24", updated_at: "2018-05-07 21:13:24">]>
.select just alters the SELECT portion of the query. The result returned is still an ActiveRecord::Relation containing model instances.
ActiveRecord will automatically create an attribute for cnt:
irb(main):047:0> #tab_communities.map(&:cnt)
=> [1]
.pluck on the other hand just pulls the column values and returns an array or array of arrays if the query contains multiple columns.
#tab_communities = TabCommunity
.where(id: _tab_community_ids)
.left_outer_joins(:tab_community_accounts)
.includes(:ref_platforms) # consider if you actually need this
.group(:id)
.order(updated_at: :desc)
.pluck('tab_communities.id, count(tab_community_accounts.id) AS cnt')
(1.0ms) SELECT tab_communities.id, count(tab_community_accounts.id) AS cnt FROM "tab_communities" LEFT OUTER JOIN "tab_community_accounts" ON "tab_community_accounts"."tab_community_id" = "tab_communities"."id" WHERE "tab_communities"."id" = 1 GROUP BY "tab_communities"."id" ORDER BY "tab_communities"."updated_at" DESC
=> [[1, 1]]
Using .* with pluck is not a good idea since you don't know what order the attributes have in the resulting array.

Compare associations on different ActiveRecords without fetching from the DB

I would like to be able to compare associated records on ActiveRecords, without actually fetching from the database. The following will do that comparison, but hits the DB when I make the comparison
employee1 = Employee.find_by(name: 'Alice')
DEBUG Employee Load (92.0ms) SELECT "employees".* FROM "employees" WHERE "employees"."name" = 'Alice' LIMIT 1
employee2 = Employee.find_by(name: 'Bob')
DEBUG Employee Load (92.0ms) SELECT "employees".* FROM "employees" WHERE "employees"."name" = 'Bob' LIMIT 1
employee1.manager == employee2.manager
DEBUG Employee Load (697.9ms) SELECT "employees".* FROM "employees" WHERE "employees"."id" = $1 ORDER BY "employees"."id" ASC LIMIT 1 [["id", 53]]
DEBUG Employee Load (504.1ms) SELECT "employees".* FROM "employees" WHERE "employees"."id" = $1 ORDER BY "employees"."id" ASC LIMIT 1 [["id", 53]]
=> true
I can compare the values of the foreign columns directly, but that's less idiomatic and can be difficult to refactor later on:
employee1.manager_id == employee2.manager_id
=> true
EDIT: I've added my own answer as a solution to this question below
If you know you're going to be needing/using the Manager for the Employee during the operation, you can make sure you load that object when the employee is loaded, that will prevent the trip back to the database:
employee1 = Employee.includes(:manager).find_by(name: 'Alice')
employee2 = Employee.includes(:manager).find_by(name: 'Bob')
employee1.manager == employee2.manager
=> true # database hit not needed...
That or just compare the IDs, but make a helper method on Employee like
class Employee
def same_manager?(other_employee)
other_employee.manager_id == self.manager_id
end
end
At least that way it's given a name and the operation within it makes sense in context.
I'm going to post my own answer for the time being. I've monkey-patched Active Record to include a new method, compare_association, which allows you to compare foreign objects on different ActiveRecords without hitting the DB.
module ActiveRecord
class Base
def compare_association(association_name, record)
association = self.class.reflect_on_association(association_name)
foreign_key = association.foreign_key
return self.read_attribute(foreign_key) == record.read_attribute(foreign_key)
end
end
end
Example:
# Compare the 'manager' association of `employee1` and `employee2`
# Equivalent to `employee1.manager_id == employee2.manager_id` but without
# referencing the DB columns by name.
employee1.compare_association(:manager, employee2)

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