Rails 3 complex query with sort - ruby-on-rails

I have a User model and an Event model. User has_many events. What I want to be able to do is find the User that has the most events created in the last 24 hours.
Ideally, the output format would have be an array of hashes [{User => [Event, Event, ...]}], which would be sorted by User's with the highest events.count
Thanks

This works for me, but might require tweaking depending on your database.
class User
scope :by_most_events, -> {
joins(:events)
.select("users.*, count(events.id) as event_count")
.where(events: { created_at: 24.hours.ago..Time.now })
.group("users.id, events.id") # this is for postgres (required group for aggregate)
.order("event_count desc")
}
end
### usage
User.by_most_events.limit(1).first.event_count
# => 123
As Sixty4Bit mentioned, you should use counter_cache, but it will not suffice in this situation because you need to query for events created in the last 24 hours.

Related

What is the best way to structure this model/DB assiociations

I am creating a project, and I want to structure the databases in the best way possible. I have a model called Event, now i would like a single event (think a baseball game) to belong to a month (i.e August) and belong to a category (i.e baseball) AND belong to a user (i.e current_user)
In otherwords, I would like a month, a category, and a user to have many events, but I don't want the events to be duplicated.
The same event object would be returned by:
august.events.find_by_id(1)
baseball.events.find_by_id(1)
current_user.events.find_by_id(1)
Any tips? My initial thought is to just set up the relations as I described above.. But I am curious to what is the best approach to creating an event object since:
august.events.new("foo")
baseball.events.new("foo")
current_user.events.new("foo")
would all yield three different event objects, when I am looking to just create one.
Can I do something like this?
Event.new(:category => "baseball", :month => "august")
then what would be the best approach to declaring a user "has that" event, and from there run commands such as baseball.events.all and august.events.all. And how would I dive deeper into the association madness if I wanted say Category -> Sports -> Baseball -> event
Thanks for any help, it is much appreciated.
this isn't an answer, per se. It is mainly to give you an idea of the flexibility of the API.
The schema, from what I can tell, is correct. Event has month and category (or category_id) and user_id. You can create all manner of helper methods to operate on that model.
For example, with a few scopes:
class Event
scope :for_month, ->(m) { where(month: m) }
scope :for_category, ->(c) { where(category: c) }
scope :for_user, ->(u) { where(user: u) }
end
# usage
Event.for_month("august").for_category("baseball").for_user(current_user)
# below, month "august" will be set on instance
current_user.events.for_month("august").new("foo")
But all of this is pointless. With those relationships in place, you can access the Event from User or Category and fill in the details yourself:
Event.where(month: "august", category: "baseball", user: current_user)
current_user.events.where(month: "august", category: "baseball")
current_user.events.new(month: "august", category: "baseball")
There are a tonne of ways to access and manipulate this data. I'm not sure you can reduce it much further than the above. You could go crazy and add a scope with multiple arguments:
class Event
scope :for_mcu, ->(m, c, u) {
for_month(m).for_category(c).for_user(u)
}
end
# usage
Event.for_mcu("august", "baseball", current_user)

Select the complement of a set

I am using Rails 3.0. I have two tables: Listings and Offers. A Listing has-many Offers. An offer can have accepted be true or false.
I want to select every Listing that does not have an Offer with accepted being true. I tried
Listing.joins(:offers).where('offers.accepted' => false)
However, since a Listing can have many Offers, this selects every listing that has non-accepted Offers, even if there is an accepted Offer for that Listing.
In case that isn't clear, what I want is the complement of the set:
Listing.joins(:offers).where('offers.accepted' => true)
My current temporary solution is to grab all of them and then do a filter on the array, like so:
class Listing < ActiveRecord::Base
...
def self.open
Listing.all.find_all {|l| l.open? }
end
def open?
!offers.exists?(:accepted => true)
end
end
I would prefer if the solution ran the filtering on the database side.
The first thing that comes to mind is to do essentially the same thing you're doing now, but in the database.
scope :accepted, lambda {
joins(:offers).where('offers.accepted' => true)
}
scope :open, lambda {
# take your accepted scope, but just use it to get at the "accepted" ids
relation = accepted.select("listings.id")
# then use select values to get at those initial ids
ids = connection.select_values(relation.to_sql)
# exclude the "accepted" records, or return an unchanged scope if there are none
ids.empty? ? scoped : where(arel_table[:id].not_in(ids))
}
I'm sure this could be done more cleanly using an outer join and grouping, but it's not coming to me immediately :-)

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.

How can I group items in a stream together if they belong to the same user

I have a stream of activities (Activity) belonging to users and with a created_at. I'd like to present a stream of the 10 latest activities, with the caveat that I want to group activities together. I know I can use group_by, but I don't want to group everything together by user, only those activities that appear > 2 times consecutively.
Tom said something
Joe did something
Joe kicked something
Joe punched something
Joe said something
Bill did something
Bill sent something
If it was grouped, it would be like so:
Tom said something
Joe did something
Joe kicked something [More from Joe...]
Bill did something
Bill sent something
Where the More to link would open up a hidden div.
Enumerable#chunk: http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-chunk
I don't think there's any special way to do this. You just have to do it.
Let's say you could efficiently grab all activities and their associated users. You likely can't do this efficiently, for something as frequent as an activity on a website, but hypothetically let's say you could, for simplicity's sake.
What you want is to iterate your activities, and collect lines you want to display. The easy road, in my humble opinion, would simply be a finder-like method on your Activity model, that returns arrays of consecutive actions by a user.
Leave your thresholds to the view. (No more than 2 consecutive actions, no more than 10 lines total.)
If Ruby has some fancy way of collecting consecutive elements from an array, then I'm probably going to look stupid, but here's some (untested) code:
class Activity < ActiveRecord::Base
belongs_to :user
def self.each_consecutive_group(options={})
default_options = { :order => 'created_at DESC' }
current_group = nil
last_user_id = false
self.all(default_options.merge(options)).each do |activity|
# Look for consecutive activities from the same user.
# (This is never true on the first iteration.)
if last_user_id == activity.user_id
current_group << activity
else
# The nil case happens during first iteration.
yield current_group unless current_group.nil?
# Initialize state for the next group of activities.
current_group = [activity]
last_user_id = activity.user_id
end
end
# Do we still have something to yield?
yield current_group if current_group
end
# Just return the above as an array, rather than iterating.
def self.consecutive_groups(options={})
groups = []
self.each_consecutive_group(options) { |group| groups << group }
groups
end
end
# Usage:
Activity.each_grouped(:include => :user, :limit => 30) do |group|
# [...]
end
# Or if you want to pass something from the controller to the view:
#activity_groups = Activity.grouped(:include => :user, :limit => 30)
To then mitigate the performance problem, I'd simply take a safe margin. If 10 lines is what you want, you could pick a scope of 20 activities to search through, and there would be room for 10 records to be hidden. Any more, and you might see less than 10 lines. Selecting 20 records is hardly a lot though, and you may choose to expand that number.
If you're die-hard on always showing 10 lines, then you probably have to move the threshold code into the above any ways. You could then use find_in_batches, rather than :limit, and keep looping until your threshold is exceeded.

Filtering ActiveRecord queries by multiple unique attributes and combining results

In my rails project, I have a query which finds the 10 most recent contests and orders by their associated poll dates:
#contests = Contest.find(
:all,
:limit => "10",
:include => :polls,
:order => "polls.start_date DESC" )
Currently this shows each contest and then iterates through associated polls sorting the master list by poll start date.
Some of these contests have the same :geo, :office and :cd attributes. I would like to combine those in the view, so rather than listing each contest and iterating through each associated poll (as I'm doing right now), I'd like to iterate through each unique combination of :geo, :office and :cd and then for each "group," iterate through all associated polls regardless of associated contest and sort by polls.start_date. I'd like to do this without having to create more cruft in the db.
Unless I've misunderstood, I think you might be looking for this:
#contests.group_by { |c| [c.geo, c.office, c.cd] }
It gives you a Hash, keyed on [c.geo, c.office, c.cd], each entry of which contains an Array of the contests that share the combination.

Resources