I'm looping through companies and collecting all of the employee names for each company:
companies = Company.all
companies.each do |company|
employee_names = company.employees.pluck(:name)
# do work, I still need access to the company object
end
That will do a separate query to get the employee names for each company. The Bullet gem suggested to add includes(:employees). So I attempted:
companies = Company.includes(:employees)
companies.each do |company|
employee_names = company.employees.collect(&:name)
# do work
end
This successfully minimizes the amount of queries, but the request actually takes longer! I'm assuming that is because the includes loads the entire set of employees into memory, but I only need the name.
Is there a better way to do an includes but only on a specific column (or columns)?
The better way is DB specific. For PostgreSQL:
SELECT_CLAUSE = <<~SQL
companies.*,
array(
SELECT name FROM employees WHERE company_id = companies.id
) as employee_names
SQL
# ...
companies = Company.select(SELECT_CLAUSE)
companies.each do |company|
# company.employee_names # => Array
end
Or don't instantiate model instances at all:
PLUCK_COLUMN = 'array(SELECT name FROM employees WHERE company_id = companies.id)'
# ...
companies = Company.pluck( :id, :title, PLUCK_COLUMN )
# [
# [ 1, 'Apple', [ 'John', 'Vasya', ... ] ],
# ...
Don't forget about indexes and pagination if you have a lot of data.
Do you have Company indexed on employee_id. It seems to me that the issue is that you're now doing work in the query that you used to do in the application memory. If you want to query company information based on certain employees, which seems to be the case, you will want to have the Company table to have an index on employee_id.
I do not know of any built-in Rails solution for this special use case.
However, you might be able to solve it by emulating sort of what includes does, with the difference that you call pluck in the end:
companies = Company.all.to_a
#=> [#<Company:...>, #<Company:...>, ...]
company_ids = companies.map(&:id)
#=> [1, 2, 3, 5, ...]
company_employee_map = Employee.
where(company_id: company_ids).
pluck(:company_id, :name).
inject({}) { |hash, (company_id, name)|
hash[company_id] ||= []
hash[company_id] << name
hash
}
companies.each do |company|
employee_names = company_employee_map[company.id]
end
The code is untested, so it might need some adjustments.
Related
I need to make a consolidated list of payable services, with filtering, ordering and pagination (with willpaginate gem) the payable services are stored in two separate models, Application and Support, both have similar fields (that I need to show in the columns) but I would need to have both in the same query.
here's my situation:
class Application < ActiveRecord::Base
// name, created_at, price ...
has_many :supports
end
class Support < ActiveRecord::Base
// name, application_id, created_at, price ...
belongs_to :application
end
and the controller:
class PaymentsController < ApplicationController
FILTER_STRINGS = {
requested: { reference_status: 'requested' },
sent: { reference_status: 'sent' },
completed: { reference_status: 'completed' }
}
ORDER_STRINGS = {
name: 'name %1$s',
price: 'price %1$s',
date: 'created_at %1$s'
}
def payable_list
#applications = Application.all
#applications = #applications.where(FILTER_STRINGS[filter_params[:filter].to_sym]) if filter_params[:filter]
order_string = format(ORDER_STRINGS[sort_params[:sort].downcase.to_sym], sort_params[:dir])
#applications = #applications.order(order_string)
#applications = #applications.paginate(page: params[:page] || 1, per_page: 20)
end
...
end
The controller currently only loads the Application, but I need to include the Support items as well, in the view I will append the string '(Support)' for support items, to differentiate the items.
How could I include both models in the same ActiveRecord_Relation, so that I can sort them together? I would have no issues with using Ruby to sort the lists, but then I'd have problems with will paginate who seems to only work with ActiveRecord_Relation.
I tried using a join clause, but that still returns an Application object
Application.joins('JOIN supports ON "supports"."application_id" = "applications"."id"')
I tried union as well, with this command:
Application.find_by_sql("SELECT
'application' AS role,
applications.id AS id,
applications.price AS price,
applications.application_id AS application_or_support,
to_char(applications.created_at, 'DD Month YYYY, HH24:MI') AS created_at,
FROM applications
UNION
select
'support' AS role,
supports.id AS id,
supports.price AS price,
supports.support_id AS application_or_support,
to_char(supports.created_at, 'DD Month YYYY, HH24:MI') AS created_at,
from supports;")
but it's only giving me the columns that have a match in the applications model.
I'm thinking that maybe I could make a new model not backed by a DB table, where the rows would originate from that union query, but I wouldn't know how to go about the syntax to get it done.
Order has_many Items is the relationship.
So let's say I have something like the following 2 orders with items in the database:
Order1 {email: alpha#example.com, items_attributes:
[{name: "apple"},
{name: "peach"}]
}
Order2 {email: beta#example.com, items_attributes:
[{name: "apple"},
{name: "apple"}]
}
I'm running queries for Order based on child attributes. So let's say I want the emails of all the orders where they have an Item that's an apple. If I set up the query as so:
orders = Order.joins(:items).where(items: {name:"apple"})
Then the result, because it's pulling at the Item level, will be such that:
orders.count = 3
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com", "beta#example.com"]
But my desired outcome is actually to know what unique orders there are (I don't care that beta#example.com has 2 apples, only that they have at least 1), so something like:
orders.count = 2
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
How do I do this?
If I do orders.select(:id).distinct, this will fix the problem such that orders.count == 2, BUT this distorts the result (no longer creates AR objects), so that I can't iterate over it. So the below is fine
deduped_orders = orders.select(:id).distinct
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
But then the below does NOT work:
deduped_orders.each do |o|
puts o.email # ActiveModel::MissingAttributeError: missing attribute: email
end
Like I basically want the output of orders, but in a unique way.
I find using subqueries instead of joins a bit cleaner for this sort of thing:
Order.where(id: Item.select(:order_id).where(name: 'apple'))
that ends up with this (more or less) SQL:
select *
from orders
where id in (
select order_id
from items
where name = 'apple'
)
and the in (...) will clear up duplicates for you. Using a subquery also clearly expresses what you want to do–you want the orders that have an item named 'apple'–and the query says exactly that.
use .uniq instead of .distinct
deduped_orders = orders.select(:id).uniq
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
If you want to keep all the attributes of orders use group
deduped_orders = orders.group(:id).distinct
deduped_orders.each do |o|
puts o.email
end
#=> output: "alpha#exmaple.com", "beta#example.com"
I think you just need to remove select(:id)
orders = Order.joins(:items).where(items: {name:"apple"}).distinct
orders.pluck(:email)
# => ["alpha#exmaple.com", "beta#example.com"]
orders = deduped_orders
deduped_orders.each do |o|
puts o.email # loop twice
end
companies = Company.all
companies.each do |company|
company.locations = current_user.locations.where(company_id: company.id)
end
return companies
Is there a better way of doing what I'm trying to do above, preferably without looping through all companies?
You need to use includes, given you did set up relationships properly:
companies = Company.all.includes(:locations)
I would be concerned here because you dont use any pagination, could lead to tons of data.
There are many articles explaining in depth like this
After your edit, I'd say
companies = Company.all
locations_index = current_user.locations.group_by &:company_id
# no need for where(company_id: companies.map(&:id)) because you query all companies anyway
companies.each do |company|
company.locations = locations_index.fetch(company.id, [])
end
This way, you have 2 queries max.
My Track model has_and_belongs_to_many :moods, :genres, and :tempos (each of which likewise has_and_belongs_to_many :tracks).
I'm trying to build a search "filter" where users can specify any number of genres, moods, and tempos which will return tracks that match any conditions from each degree of filtering.
An example query might be
params[:genres] => "Rock, Pop, Punk"
params[:moods] => "Happy, Loud"
params[:tempos] => "Fast, Medium"
If I build an array of tracks matching all those genres, how can I select from that array those tracks which belong to any and all of the mood params, then select from that second array, all tracks which also match any and all of the tempo params?
I'm building the initial array with
#tracks = []
Genre.find_all_by_name(genres).each do |g|
#tracks = #tracks | g.tracks
end
where genres = params[:genres].split(",")
Thanks.
I'd recommend using your database to actually perform this query as that would be a lot more efficient.
You can try to join all these tables in SQL first and then using conditional queries i.e. where clauses to first try it out.
Once you succeed you can write it in the Active Record based way. I think its fairly important that you write it in SQL first so that you can properly understand whats going on.
This ended up working
#tracks = []
Genre.find_all_by_name(genres).each do |g|
g.tracks.each do |t|
temptempos = []
tempartists = []
tempmoods = []
t.tempos.each do |m|
temptempos.push(m.name)
end
tempartists.push(t.artist)
t.moods.each do |m|
tempmoods.push(m.name)
end
if !(temptempos & tempos).empty? && !(tempartists & artists).empty? && !(tempmoods & moods).empty?
#tracks.push(t)
end
end
end
#tracks = #tracks.uniq
Say I have two relations that hold records in the same model, such as:
#companies1 = Company.where(...)
#companies2 = Company.where(...)
How can I find the intersection of these two relations, i.e. only those companies that exist within both?
By default connecting those where together creates AND which is what you want.
So many be:
class Company < ActiveRecord::Base
def self.where_1
where(...)
end
def self.where_2
where(...)
end
end
#companies = Company.where_1.where_2
====== UPDATED ======
There are two cases:
# case 1: the fields selecting are different
Company.where(:id => [1, 2, 3, 4]) & Company.where(:other_field => true)
# a-rel supports &, |, +, -, but please notice case 2
# case 2
Company.where(:id => [1, 2, 3]) & Company.where(:id => [1, 2, 4, 5])
# the result would be the same as
Company.where(:id => [1, 2, 4, 5])
# because it is &-ing the :id key, instead of the content inside :id key
So if you are in case 2, you will need to do like what #apneadiving commented.
Company.where(...).all & Company.where(...).all
Of course, doing this sends out two queries and most likely queried more results than you needed.
I solve similar problem this way
Company.connection.unprepared_statement do
Company.find_by_sql "#{#companies1.to_sql} INTERSECT #{#companies2.to_sql}"
end
We need unprepared_statement block here because latest Rails versions use prepared statements to speed up arel queries, but we need pure SQL in place.
Use sql keyword INTERSECT.
params1 = [1,2,4]
params2 = [1,3,4]
query = "
SELECT companies.* FROM companies
WHERE id in (?,?,?)
INTERSECT
SELECT companies.* FROM companies
WHERE id in (?,?,?)
"
Company.find_by_sql([query, *params1, *params2])
it will be faster than previous solution.
You could use ActiveRecord::SpawnMethods#merge
Example:
Company.where(condition: 'value').merge(Company.where(other_condition: 'value'))
For anyone who is stuck with Rails4 and cant use Rails5 .or syntax:
I had a dynamically number of big queries, which had similar conditions ( and therefore also similar results). My rake server would have problems when all of them at once would get instantiated, converted to arrays and then merged.
I needed a ActiveRecord::Relation (not fired yet) to use with find_each.
Looked something like this:
Class Conditions
def initialize
self.where_arr = []
self.joins_arr = []
end
def my_condition1
where_arr << 'customers.status = "active"'
joins_arr << :customer
end
def my_condition2
where_arr << 'companies.id = 1'
end
end
conditions = []
joins = []
# probably call them in a .each block with .send
conditions1 = Conditions.new
conditions1.my_condition1
conditions1.my_condition2
conditions << "(#{conditions1.where_arr.join(' AND ')})"
joins << conditions1.joins_arr
conditions2 = Conditions.new
conditions2.my_condition1
joins << conditions2.joins_arr
Company.joins(joins).where(conditions.join(' OR '))
=> SELECT companies.* FROM companies
INNER JOIN customers ON companies.customer_id = customers.id
WHERE (customers.status = 'active' AND companies.id = 1) OR
(customers.status = 'active')
Its kind of hacky but it works, until you can hopefully migrate to Rails 5