Finding records which belong to multiple models with HABTM - ruby-on-rails

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

Related

Rails append the correct model object from has_many association to an array of Models

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.

How to execute where condition on a joined table without querying the db in a loop

I have the following models
App has_many features
Feature has_many translations
Now I want to lookup a translation in order see to if I need to create or update.
App.joins(features: :translations).first.features.each do |feature|
languages.each do |lang|
translation = feature.translations.where(language: lang).first_or_initialize
end
end
Now the problem is that I have a db lookup for each language which is quite a problem since I have a lot of features.
How can I benefit from eager loading in order to just lookup the translation in the joined table?
Lets partition the languages into two two arrays, found and not_found:
found, not_found = languages.partition do |lang|
App.joins(:features => :translations).where("translations.language" => lang).present?
end
found_language_translations = Translation.where("language in (?)", found)
new_language_translations = not_found.map{|lang| Translation.new(language: lang) } #This will fire up too many queries though :/

Ruby/rails sort_by not ordering properly?

I have a long array of Photo model objects, and I want to sort them by created_at, newest first, then get a new array with the first 21 photos.
My problem is that the final array is not ordered properly.
Here is my code:
#recent_photos = photos.sort_by(&:created_at).reverse.first(21)
when I print out #recent_photos the created_at values are ordered like this:
1458948707
1458943713
1458947042
1458945171
...
What is the correct way to sort objects?
UPDATE:
here's how the initial list is compiled:
photos = #user.photos
#following = #user.following
#following.each do |f|
photos += f.photos if f.id != #user.id
end
#user.memberships.each do |group|
photos += group.photos
end
SOLUTION:
problem was with the question - I wanted to sort by timestamp not created_at, and those were timestamp values in the output
You can crunch it all down into a single query:
#recent_photos = Photo.where(
user_id: #user.following_ids
).order('created_at DESC').limit(21)
You really do not want to be doing N queries for each of these as it will get slower and slower as a person has more people they're following. If they follow 10,000 people that's a ridiculous number of queries.
If you add a :through definition to your model you may even be able to query the photos directly:
has_many :follower_photos,
class_name: 'Photo',
through: :followers
Whatever your constraints are, boil them down to something you can query in one shot whenever possible. If that's not practical, get it down to a predictable number of queries, never N.
Try:
#recent_photos = Photo.order('created_at desc').first(21)

Selecting average and latest value of column on associated record using rails/activerecord

I have three models:
class Customer < ActiveRecord::Base
has_many :monthly_records
belongs_to :store
end
I need want to create an index view where I show the following fields:
customer.id
customer.name
store.name
average of monthly_records.grand total where monthly_records.month_start > ? AND monthly_records.month_start <= ?
monthly_records.grand_total for a specific month that the user will supply through the controller
Because this is an index and I want to show many rows at a time I want to do this in a single query and avoid the N+1 problem. I know how to do this with Customer.select.join.group if I take 1-3 and EITHER 4 or 5, but I can't figure out how to do all 5 at once.
I've found some pure SQL answers that look like they may work, but I'd prefer to keep this to activerecord if at all possible.
Using postgres 9.3.4 and rails 4.0.10.
Just guessing, Can you try:
Controller:
#customers = Customer.
select('customers.id,customers.name,monthly_records.grand_total,stores.name').
includes(:store, :monthly_records).
references(:stores, :monthly_records)
Try Customer\ to see if dot can start the next lines:
#customers = Customer\
.select('customers.id,customers.name,monthly_records.grand_total,stores.name')...
On view:
- #customers.each do |customer|
- records = customer.monthly_records.\
where('month_start > ? AND month_start <= ?', some_param,some_param)
= customer.id
= customer.name
= customer.store.name
= records.average(:grand_total)
= records.sum(:grand_total)
I think there is one set of records to be eager loaded (records variable). But this would be a start. Can you try? a let me know which tables have the N+1 problem with this approach.
Update: Eager loading monthly records for a given month
Rails gives you the possibility to eager load an association:
Model:
has_many :this_month_monthly_records,
->{ where('month_start > :start, month_start <= :start', start: your_month) },
class_name: 'MonthlyRecord'
Controller:
#customers = Customer.
select('customers.id,customers.name,monthly_records.grand_total,stores.name').
includes(:store, :this_month_monthly_records).
references(:stores, :monthly_records)
On view:
- #customers.each do |customer|
- records = customer.this_month_monthly_records
= customer.id
= customer.name
= customer.store.name
= records.average(:grand_total)
= records.sum(:grand_total)
The problem with this way is your_month variable. It's passed in params but I see no safe way to pass it to the model.

How to rewrite this each loop in ruby?

I have this loop:
stations = Station.where(...)
stations.all.each do |s|
if s.city_id == city.id
show_stations << s
end
end
This works well, but because of looping the all the data, I think it's kinda slow. I've tried to rewrite it with using select, like this:
show_stations << stations.select { |station| station.city_id == city.id}
But the amount of saved data into show_stations is different compared to the each version and then, the data are in different format (array/object).
Is there any better/faster way to rewrite the loop version?
The fastest version of this maybe the built-in rails ActiveRecord method for finding associated objects.
So provided your Station model contains this:
class Station < ActiveRecord::Base
belongs_to :city
And your City model contains this:
class City < ActiveRecord::Base
has_many :stations
Then rails automatically generates the method city.stations which automatically fetches the stations which contain that city's id from the database. It should be pretty optimized.
If you want to make it even faster then you can add add_index :stations, :city_id to your table in a migration and it will retrieve faster. Note that this only saves time when you have a lot of stations to search through.
If you need to make it an array you can just convert it after with city.stations.to_a. And if you wanted to narrow it further, just use the select method and add the conditions that you wanted to previously add in your Station.where(...) statement.
(e.g. city.stations.to_a.select { |item| your_filter })
You should also cache the query results like
stations ||= Station.where("your where").where(:city_id => city.id)
Maybe you need to include into the where clause the city parameter:
stations = Station.where("your where").where(:city_id => city.id)
or the same
stations = Station.where("your where").where('city_id = ?', city.id)
Station appears to be an active record model. If that is the case, and you don't need all the stations, you can add the city.id filter to your where statement.
The issue you're having now is that you're adding the array returned from select as the last item of show_stations. If you want show_stations to only contain stations that match city.id then use show_stations = ... rather than show_stations << .... If you want show_stations to contain what it already contains plus the stations that match city.id then use show_stations + stations.select { |station| station.city_id == city.id }. (There are a number of other approaches for adding two arrays together.)

Resources