Sorting a Rails active record - ruby-on-rails

I have a table A(:name, :address, :country_id). I run this query
ids = [1,2,3]
#result = A.where(country_id: ids)
But, I want to get the results in this order : records containing country_id = 2 first, then 1 and then 3.
How should I write this query?
Edit :
#people = []
#result.each do |r|
#people.push([r,r.name])
end
I have an array #people of arrays now and want to sort it. What should be the query now?
Another Edit :
#people.sort_by! {|p| preferred_order.index(p.first.country_id)}
This worked.

You can use sort_by :
ids = [1,2,3]
preferred_order = [2,1,3]
#result = A.where(country_id: ids)
#result.sort_by! { |u| preferred_order.index(u.country_id) }

One option would be adding an extra column
rails g migration add_sortorder_to_A sort_order:integer
Then add a
before_save :update_sort_order
def update_sort_order
if self.country_id == 1
self.update_attributes(sort_order:2)
elsif self.country_id ==2
self.update_attributes(sort_order:1)
........
end
Alternatively, put your sort_order in the countries and sort on that.
Alternatively, create a dynamic field which will give you the sort_order.
There is probably a more elegant way of doing this, but this should work in a quick and dirty way and allow you to use standard activerecord queries and sorting. (i.e. you can just forget about it once you've done it the once)

Related

Select from DB all records or by array if present

In RoR app I want to write a model method that will return some records.
It should select by ids if ids are present or all records if not
def viewing_duration(ids = nil)
if ids
tracks.where(video_id: ids).sum('age(updated_at, created_at)')
else
tracks.sum('age(updated_at, created_at)')
end
end
Question:
How I can write query in one line and pass the expression right to where method?
Something like this:
tracks.where(video_id: ids.nil? ? 'all' : ids).sum('age(updated_at, created_at)')
Keeping it as more lines probably makes it easier to understand. I suggest keeping it close to as it is while removing repetition:
def viewing_duration(ids = nil)
if ids
tracks.where(video_id: ids)
else
tracks
end.sum('age(updated_at, created_at)')
end
If you do want to pass something into where to find all records, you can use an sql statement that will always evaluate to true. For example:
tracks.where(ids.nil? ? '1=1' : { video_id: ids }).sum('age(updated_at, created_at)')
It is not one line, but as idea how to organize your code using chaining
def viewing_duration(ids = nil)
entities = tracks
entities = entities.where(video_id: ids) if ids
entities.sum('age(updated_at, created_at)')
end

Remove similar records when iterating an ActiveRecord relation in Rails

I would like to find the best way to remove items from an array during an iteration of ActiveRecord results.
I want to remove the record from the array, not from the database. So I can't use a.delete(record) for example.
# find similar items #items in a AR relation array
#items = Item.where("toto = ?", value).to_a
#items.each do |i|
# match similar records using postgres
#similar_items = ...
# now try to remove similar items from original #items array
#similar_items.each do |similar_item|
# this seems to not work
#items.reject! { |item_to_remove|
item_to_remove == similar_item
end
end
end
In fact, my goal is to remove all duplicates from the array using similar items matched to not reiterate them.
I would propose you the following approach:
Collect all similar items
Return collection where you have excluded these similar items.
It should simplify your code and make fewer iterations.
#items = Item.where("toto = ?", value).to_a
similar_items = #items.reduce([]) do |r, i|
#similar_items = ...
(r + #similar_items).flatten
end
filtered_collection = #items.reject { |item| similar_items.include?(item) }

Use Ruby's select method on a Rails relation and update it

I have an ActiveRecord relation of a user's previous "votes"...
#previous_votes = current_user.votes
I need to filter these down to votes only on the current "challenge", so Ruby's select method seemed like the best way to do that...
#previous_votes = current_user.votes.select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
But I also need to update the attributes of these records, and the select method turns my relation into an array which can't be updated or saved!
#previous_votes.update_all :ignore => false
# ...
# undefined method `update_all' for #<Array:0x007fed7949a0c0>
How can I filter down my relation like the select method is doing, but not lose the ability to update/save it the items with ActiveRecord?
Poking around the Google it seems like named_scope's appear in all the answers for similar questions, but I can't figure out it they can specifically accomplish what I'm after.
The problem is that select is not an SQL method. It fetches all records and filters them on the Ruby side. Here is a simplified example:
votes = Vote.scoped
votes.select{ |v| v.active? }
# SQL: select * from votes
# Ruby: all.select{ |v| v.active? }
Since update_all is an SQL method you can't use it on a Ruby array. You can stick to performing all operations in Ruby or move some (all) of them into SQL.
votes = Vote.scoped
votes.select{ |v| v.active? }
# N SQL operations (N - number of votes)
votes.each{ |vote| vote.update_attribute :ignore, false }
# or in 1 SQL operation
Vote.where(id: votes.map(&:id)).update_all(ignore: false)
If you don't actually use fetched votes it would be faster to perform the whole select & update on SQL side:
Vote.where(active: true).update_all(ignore: false)
While the previous examples work fine with your select, this one requires you to rewrite it in terms of SQL. If you have set up all relationships in Rails models you can do it roughly like this:
entry = Entry.find(params[:entry_id])
current_user.votes.joins(:challenges).merge(entry.challenge.votes)
# requires following associations:
# Challenge.has_many :votes
# User.has_many :votes
# Vote.has_many :challenges
And Rails will construct the appropriate SQL for you. But you can always fall back to writing the SQL by hand if something doesn't work.
Use collection_select instead of select. collection_select is specifically built on top of select to return ActiveRecord objects and not an array of strings like you get with select.
#previous_votes = current_user.votes.collection_select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
This should return #previous_votes as an array of objects
EDIT: Updating this post with another suggested way to return those AR objects in an array
#previous_votes = current_user.votes.collect {|v| records.detect { v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id}}
A nice approach this is to use scopes. In your case, you can set this up the scope as follows:
class Vote < ActiveRecord::Base
scope :for_challenge, lambda do |challenge_id|
joins(:entry).where("entry.challenge_id = ?", challenge_id)
end
end
Then your code for getting current votes will look like:
challenge_id = Entry.find(params[:entry_id]).challenge_id
#previous_votes = current_user.votes.for_challenge(challenge_id)
I believe you can do something like:
#entry = Entry.find(params[:entry_id])
#previous_votes = Vote.joins(:entry).where(entries: { id: #entry.id, challenge_id: #entry.challenge_id })

Finding records which belong to multiple models with HABTM

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

How do I sort in ruby/rails on two fields?

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)

Resources