Ruby/rails sort_by not ordering properly? - ruby-on-rails

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)

Related

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.

How to select from a table that has been joined with same model/class/table?

I'm trying to get a count of how many subcontacts every contact has.
Class Contacts
has_many :subcontacts, class_name: "Contact",foreign_key: "supercontact_id"
belongs_to :supercontact, class_name:"Contact"
And here's the activerecord part i have so far that's roughly what i'm trying to do.
Contact.joins{subcontacts.outer}.select(subcontacts.count as subcontact_count)
I think the problem is that the joins portion is looking for a association name and the select part is looking for a table name. The trouble is that the table name is the same table... What's the best way to do this so that it stays as a relation or using SQL so that we can minimize the number of queries so that it isn't an N+1 problem?
Try using
results = Contact.joins(:subcontacts).select("count(subcontacts.id) as count, contacts.id").group("contacts.id")
and count can be fetched as
results.map do |result|
"Contact ID: #{result.id} - Subcontacts Count: #{result['count']}"
end
Contacts.all.each do |contact|
puts contact.name => contact.subcontacts.count
end
OR
Contacts.all.map{|contact| [contact.name => contact.subcontacts.count]}
The above will provide you the hash like answer{contact_name => subcontacts.count}

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 to Average Multiple Columns in Rails

I have the following objects: Products, Ratings, and Users. I am trying to filter and select a number of Products that the User owns (through a has_many :through relationship with UserProducts) and average a certain column the Ratings table that matches their User ID and the correct Product ID.
So, my function is something along these lines:
def find_rating(criteria)
product = self.products.find(:all, :conditions => ["criteria = ?", criteria])
rating = self.ratings.where("product_id = ?", product).average(:overall)
end
I think that I'm going about this the wrong way, because I'm trying to find a product_id by passing an entire array of data consisting of multiple products. But, I think of using a more traditional loop and that seems convoluted. Can someone point me in the right direction for solving this problem? Thanks!
If product is a single entry, as it appears to be in your code, I would do this:
rating = self.products.find_by_criteria(criteria).ratings.average(:overall)
If it's an array of products, this method may help you: http://apidock.com/rails/ActiveRecord/Batches/ClassMethods/find_each

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources