I'm wondering if there's an efficient way to combine the results of multiple ActiveRecord objects in Rails. For example, I might make three individual calls to three individual tables, and I want the results combined, and sorted by a common column.
Here's a super basic code example that will hopefully make my question easier to understand:
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
#combined_results_sorted_by_date_column = (how?)
As suggested by others, here's one solution to the problem.
#combined_results = #result1 + #result2 + #result3
#combined_results.sort! {|x,y| x.date <=> y.date}
What if I want to sort by date, but Table3 refers to the "created_on" column as date?
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
#combined_results_sorted_by_date_column =
(#results1 + #results2 + #results3).sort_by(&:date)
What if I want to sort by date, but Table3 refers to the "created_on" column as date?
class Table3
alias_method :date, :created_on
end
or simply
class Table3
alias date created_on
end
You don't work with "Tables" but rather objects.
If you think about it this way, it would make no sense to have:
#results1 = Users.find(:all)
#results2 = Posts.find(:all)
#results3 = Comments.find(:all)
What would the "combined" form of it means?
What you probably want is to combine results from the same kind using different "queries".
Is that it?
You're probably not going to like this answer, but I would say you might want to revise your database schema. I was in a similar situation, and sorting the results after concatenating them is definitely not the way you want to go.
#results1 = Table1.find(:all)
#results2 = Table2.find(:all)
#results3 = Table3.find(:all)
combined = (#results1 + #results2 + #results3).sort { |x, y| x.date <=> y.date }
#combined_results = #result1 + #result2 + #result3
#combined_results.sort! {|x,y| x.date <=> y.date}
While this surely is not be the most efficient code in the world, it might just be what you need.
If some models don't have a date method I suggest you create one.
It is as easy as.
def date
created_on
end
I'm assuming you want a mixed array of three different types of ActiveRecord objects, sorted by a date of some kind.
#array = (Bucket.all + Mop.all + Detergent.all).sort{|x,y| x.sort_by <==> y.sort_by}
Since your sort_by field is different for each object type, you need to define it.
class Bucket < ActiverRecord::Base
def sort_by
cleaned_on
end
end
class Detergent < ActiverRecord::Base
def sort_by
purchased_on
end
end
You could pull in all the data sorted in a single query using UNION, but you wouldn't get AR objects out of that.
I'm not sure what your data volumes are like, but when it comes to sorting, your database will do a better job of it than you ever will using "application layer" code.
Do you need the data returned as an array of model objects seeing as the three tables will probably generate a mixed up array of three distinct model classes ?
Why not use direct-SQL returning rows and columns and have the DB do all the hard work sorting the data ?
Related
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.
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.
I have a Deal model with a lot of associations. One of the associations is Currency. The deals table and the currencies table both have a name column. Now, I have the following ActiveRecord query:
Deal.
joins(:currency).
where("privacy = ? or user_id = ?", false, doorkeeper_token.resource_owner_id).
select("deals.name as deal_name, deals.date as deal_creation_date, deals.amount as deal_amount, currencies.name as currency_name, currencies.symbol as currency_symbol")
This query doesn't work, its result is an array of Deal objects with no attributes. According to someone on IRC, the "as" parts are incorrect because the ORM doesn't know how to assign the columns to which attribute (or something like that) which is fair enough. I tried to add attr_accessor and attr_accessible clauses though but it didn't work.
How can I make the above query work please? What I expect the result to be is an Array of Deal objects with deal_name, deal_creation_date, etc. virtual attributes.
Most likely the query is working correctly, but the returned Deal objects appear not to have any attributes when you print them out because of the way that Deal implements the inspect method.
So if you assign the result of the query to a variable you will see that this appears empty:
v.each do |deal| ; puts deal.inspect ; end
While this shows the attributes you want:
v.each do |deal| ; puts deal.to_yaml ; end
More details are in this question.
For example, I would like to sort by game_date, and then if the date is the same, sort it by team? What would be the best way to do this?
#teams = #user.teams
#games = #teams.reduce([]) { |aggregate, team| aggregate + team.games}.sort_by(&:game_date)
The best way would be to have your database do it, but if you want to use Ruby:
#games = #data.sort_by {|x| [x.game_date, x.team] }
The sorting behaviour of Array is to sort by the first member, then the second, then the third, and so on. Since you want the date as your primary key and the team as the second, an array of those two will sort the way you want.
#teams = #user.teams
#games = #teams.games.order("game_date ASC, team ASC")
#teams = #user.teams
#games = #teams.games.order(game_date: :asc, team: :asc)
Assuming that you have a model which have these two fields
Model.all(:order => 'attribute1, attribute2')
Incase the fields are in multiple tables, you can use joins.
For those looking to sort an array containing two different objects w/ a differently named date field, you can do this with an attribute alias method in the model.
note: using .sort_by! destructively doesn't work.
News.rb
def release_date
self.publish_date
end
Controller
#grid_elements = #news + #albums
#grid_elements = #grid_elements.sort_by(&:release_date)
Looking for a simple method utilizing active record to grab data from two models, combine the data, and then sort the combined output by created_at.
For example:
assume two models, Comment & Like both belongs_to User
return a combined list of #user's comments and likes sorted by date
I know I can do this in SQL but I'd really like an active record solution.
Thanks!
I believe it should be as simple as:
combined_sorted = (User.comments + User.likes).sort{|a,b| a.created_at <=> b.created_at }
How about something like (untested):
class User < ActiveRecord::Base
scope :activities_by_date, :joins(:comments).joins(:likes).order("created_at desc")
end
Then you can do #user.activities_by_date and let the db do all the work