Best way to query without a join table ActiveRecord - ruby-on-rails

So I have a table trips and a table conditions. Trips has a start_date and end_date, and neither last more than a few hours on the same day. Conditions has a date column. I am trying to compare them to find the trip.id of a date in order to seed with a :trip_id foreign key located in the conditions table with a very large csv. I understand that it is a many_to_many relationship, but I am wondering how to convert what I wrote in Ruby (transitioning to rails currently) to the "ActiveRecord" way without hitting the database so many times.
Here is my code:
Helper method
def self.id_by_date(date)
find_by(date: date).id
end
Main method
def self.sort_temp(range)
array = where(max_temperature: range).all.map {|condition| condition.date}
trip_nums = array.map do |date|
Trip.where(start_date: date.beginning_of_day...date.end_of_day).count
end
output = {}
output[:max] = trip_nums.sort.last
output[:min] = trip_nums.sort.reverse.last
output[:avg] = trip_nums.inject(:+) / trip_nums.length unless trip_nums.length == 0
output
end
EDIT: To be clear, I am trying to find all conditions associated with a day

Related

Rails 5.1.: get records from association for each datetime interval

I have Model named Task with method dates which gives me this output (it can be different and multiple DateTime intervals):
[{:from=>2017-09-04 00:00:00 +0300, :to=>2017-09-04 05:59:59 +0300},
{:from=>2017-09-05 12:00:00 +0300, :to=>2017-09-05 19:59:59 +0300}
then in same Model there is another method inventory, where for each task I want to get corresponding Inventory model records.
At the moment I'm trying to do something like this:
self.inventories.where(date: self.from..self.to)
This does not work, because from and to comes from data structure above and there are no such columns in my DB. I've set appropriate association in my model and I can do self.inventories.
Is there a way to do Rails query in order to grab matching records for each of my dates intervals, please? Thank you.
if your asking how to access data inside an array, you can loop through each data.
def self.dates
#generate intervals as array of hashes
end
def self.inventories
[].tap do |inventories|
dates.each do |date|
from = date[:from]
to = date[:to]
inventories << self.inventories.where(date: from..to)
end
end
end
and ensure to use #flatten since it would be nested in arrays
inventories.flatten
hope this helps

Ruby on Rails inefficient database query

My Rails app has the following conditions:
Each Style has many Bookings
Each Booking has a single warehouse value, and a single netbooked value
I need to update the warehouse_netbooked column of every Style with a hash containing the total netbooked sum for each warehouse across all of the style's bookings.
My current code works, but is way too slow (each iteration is taking ~0.5s, and there are thousands of styles):
def assign_warehouse_bookings
warehouses = ["WH1","WH2","WH3"]
Style.all.each do |s|
style_warehouse_bookings = Hash.new
warehouses.each do |wh|
total_netbooked = s.bookings.where(warehouse: wh).sum(:netbooked)
style_warehouse_bookings[wh] = total_netbooked
end
s.update(warehouse_netbooked: "#{style_warehouse_bookings}")
end
end
Here a small change to your code to avoid to do many queries following #eric-duminil advise
#styles = Style.includes(:bookings)
#styles.each do |s|
style_warehouse_bookings = Hash.new
warehouses.each do |wh|
total_netbooked = s.bookings.map {|book| book.warehouse.eql?(wh) ? book.netbooked : 0}.sum
style_warehouse_bookings[wh] = total_netbooked
end
s.update(warehouse_netbooked: "#{style_warehouse_bookings}")
end
end
I hope it help you.
In your case, you have for a single style, you are fetching booking with 3 type of warehouses finding sum of netbooked for each type of warehouse. This is highly inefficient.
One good rule is, first fetch required data from database, and after fetching data use ruby to handle data. It's important to fetch only required data. So, in your case, you can fetch styles and all related bookings. Then you can iterate through collection of bookings and prepare style_warehouse_bookings hash.
use find_each instead of each
use includes or preload to preload data.
Here is simple example which will definitely improve performance,
warehouses = ["WH1","WH2","WH3"]
# preload bookings with style, `preload` used explicitely instead of `includes` to prevent cross join queries.
styles = Style.joins(:bookings).where('bookings.warehouse' => warehouses).preload(:bookings)
# find_each fetches data in batches of 1000 records
styles.find_each do |s|
style_warehouse_bookings = Hash.new
warehouses.each do |wh|
# select and sum methods of ruby are used instead of where and sum of active-record
total_netbooked = s.bookings.select{ |booking| booking.warehouse = wh }.sum(&:netbooked)
style_warehouse_bookings[wh] = total_netbooked
end
s.update(warehouse_netbooked: "#{style_warehouse_bookings}")
end
Read in depth about preload, includes and joins at eager loading associations documentation. Apart from that I wrote an article on when to use preload, includes and joins here which can help.
I think you want to do batch update. If I am correct check the following
link
Is there anything like batch update in Rails?
Also you can introduce transaction to avoid too many commits
For example
def assign_warehouse_bookings
Style.transaction do
<your remaining code goes here>
end
end

Ruby Compare Attributes in Array of Objects

I have an User model and a Concert model in my Ruby on Rails project. The User has an array containing all the concerts of his favorite bands, and the Concert model has the city and date attributes.
What I need to do is iterate through this array of concerts to see if they´re any concerts that are happening the same day in the same city.
Something like this:
#user_concerts = [concert1, concert2, concert3....]
I guess I'll have to loop over this array two times, but I'm a bit stuck with this.
The way my DB is made, I use the query below to get the IDs of the bands favorited by a user.
bands_ids_array = UsersBand.where(user_id: current_user.id, is_favorite: true).pluck(:band_id)
#user_favorite_bands = []
bands_ids_array.each do |id|
#user_favorite_bands << Band.find(id)
end
UsersBand is the relational DB between User and Band models. It contains user_id, band_id and a Boolean field is_favorite.
To get all the concerts I do the following currently:
#user_concerts = []
#user_favorite_bands.each do |band|
band.concerts.each do |concert|
#user_concerts.push(concert)
end
end
I´ve tried nesting two "each" loops, a while loop nested inside an "each" trying to compare all the concerts to see if they have the same city or date, but that didn´t work.
Edit: I did that and it worked for me:
for i in (0..#user_concerts.length) do
for j in (i+1..#user_concerts.length-1) do
if #user_concerts[i].city == #user_concerts[j].city && #user_concerts[i].date == #user_concerts[j].date
matches.push(#user_concerts[i])
matches.push(#user_concerts[j])
end
end
Any help will be appreciated. Thanks a lot guys.

How to efficiently update associated collection in rails (eager loading)

I have a simple association like
class Slot < ActiveRecord::Base
has_many :media_items, dependent: :destroy
end
class MediaItem < ActiveRecord::Base
belongs_to :slot
end
The MediaItems are ordered per Slot and have a field called ordering.
And want to avoid n+1 querying but nothing I tried works. I had read several blogposts, railscasts etc but hmm.. they never operate on a single model and so on...
What I do is:
def update
#slot = Slot.find(params.require(:id))
media_items = #slot.media_items
par = params[:ordering_media]
# TODO: IMP remove n+1 query
par.each do |item|
item_id = item[:media_item_id]
item_order = item[:ordering]
media_items.find(item_id).update(ordering: item_order)
end
#slot.save
end
params[:ordering_media] is a json array with media_item_id and an integer for ordering
I tried things like
#slot = Slot.includes(:media_items).find(params.require(:id)) # still n+1
#slot = Slot.find(params.require(:id)).includes(:media_items) # not working at all b/c is a Slot already
media_items = #slot.media_items.to_a # looks good but then in the array of MediaItems it is difficult to retrieve the right instance in my loop
This seems like a common thing to do, so I think there is a simple approach to solve this. Would be great to learn about it.
First at all, at this line media_items.find(item_id).update(ordering: item_order) you don't have an n + 1 issue, you have a 2 * n issue. Because for each media_item you make 2 queries: one for find, one for update. To fix you can do this:
params[:ordering_media].each do |item|
MediaItem.update_all({ordering: item[:ordering]}, {id: item[:media_item_id]})
end
Here you have n queries. That is the best we can do, there's no way to update a column on n records with n distinct values, with less than n queries.
Now you can remove the lines #slot = Slot.find(params.require(:id)) and #slot.save, because #slot was not modified or used at the update action.
With this refactor, we see a problem: the action SlotsController#update don't update slot at all. A better place for this code could be MediaItemsController#sort or SortMediaItemsController#update (more RESTful).
At the last #slot = Slot.includes(:media_items).find(params.require(:id)) this is not n + 1 query, this is 2 SQL statements query, because you retrieve n media_items and 1 slot with only 2 db calls. Also it's the best option.
I hope it helps.

Rails named scope for left joined model

I have a table named acts, and I'll like to run a query that rolls up act values for a whole week. I'd like to make sure the query always returns one row for each day of the week, even if there are no records for that day. Right now I'm doing it like this:
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
connection.select_values(<<-EOQ)
SELECT COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time
FROM generate_series(0, 6) AS s(t)
LEFT JOIN acts
ON acts.day = '#{sunday}'::date + s.t
WHERE deleted_at IS NULL
GROUP BY s.t
ORDER BY s.t
EOQ
end
Is there any way I could make this a named scope on the Act class so it can be combined with other conditions, for example to filter the Acts by a client_id? Since acts isn't in my FROM, but is part of the LEFT JOIN, I'm guessing not, but perhaps someone out there knows a way.
Edit: Just to clarify, the goal is for this method to always return exactly 7 Act objects, regardless of what's in the database.
if you want your query object to be chainable it must be an ActiveRelation object
where, select, order and the other Arel objects return ActiveRelation objects that are chainable, so if the below works you can chain off of the returned query object
note in rails 3 and up having a class method that returns an ActiveRelation is basically the same as a scope, they are both chainable query objects
class Act
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
select("COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time")
.from("generate_series(0, 6) AS s(t)")
.joins("LEFT JOIN acts ON acts.day = '#{sunday}'::date + s.t")
.where("deleted_at IS NULL")
.group("s.t")
.order("s.t")
end
# ...
end
client_id = params[:client_id]
Act.this_week_totals.where("client_id = ?", client_id)
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-from
Although I really thought I could use the solution from #house9, I don't see any way to avoid compromising on at least one of these goals:
Always yield 7 Act objects.
Return an ActiveRelation so I can compose this method with other scopes.
Permit joining to the clients table.
Here is the part-SQL/part-Ruby solution I'm actually using, which sadly gives up on point #2 above and also returns tuples rather than Acts:
def self.this_week(wk=0)
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
not_deleted.where("day BETWEEN ? AND ?", sunday, sunday + 7.days)
end
scope :select_sum_total_hours,
select("EXTRACT(EPOCH FROM COALESCE(SUM(end_time - start_time), '0:00:00'))/3600 AS total_hours")
scope :select_sum_total_fees,
joins(:client).
select("SUM(COALESCE(clients.rate, 0) * EXTRACT(EPOCH FROM COALESCE(end_time - start_time, '0:00:00'))/3600) AS total_fees")
def self.this_week_totals_by_day(wk=0)
totals = Hash[
this_week(wk)
.select("EXTRACT(DAY FROM day) AS just_day")
.select_sum_total_hours
.select_sum_total_fees
.group("day")
.order("day")
.map{|act| [act.just_day, [act.total_hours.to_f, act.total_fees.to_money]]}
]
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
(0..6).map do |x|
totals[(sunday + x.days).strftime("%d")] || [0, 0.to_money]
end
end
That could be DRYed up a bit, and it would produce errors if there were ever a month with fewer than 7 days, but hopefully it shows what I'm doing. The scopes for this_week, select_sum_total_hours, and select_sum_total_fees are used elsewhere, so I want to pull them out into scopes rather than repeating them in several big raw SQL strings.

Resources