views:

109

answers:

3

Id like to create a model in rails that does not correlate to a table in the database. Instead the model should dynamically pull aggregrate data about other models.

Example:

I have a Restaurant model stored in the restaurants table in the DB. Id like to have a RestaurantStats model where i can run a RestaurantStats.find_total_visitors, or RestaurantStats.find_time_spent etc... on it and it returns a set of RestaurantStats models each with:

[:restaurant_id, :stat_value]

Obviously in each find... method that stat_value will mean something different (for find_time_spent it will be seconds spent, for find_total_visitors it will be number of visitors). The idea will be to return the top 100 restaurants by time spent, or total visitors.

So far im creating a model (not inherited from ActiveRecord)

class RestaurantStats 
   attr_reader :restaurant_id
   attr_reader :stat_value

   def self.find_total_visitors ... 
   def self.find_time_spent ...
end

The question is how do define the find_total_visitors, find_time_spent functions in a rails y way so that it will populate the restaurant_id, stat_value fields?

A: 

Set the values using self.(fieldname) and then save those value (after running a find or build).

CodeJoust
+1  A: 

Are you sure you don't just want these to be methods on Restaurant?

class Restaurant < ActiveRecord::Base
  has_many :visitors

  def total_visitors
    visitors.count # Or whatever
  end

  def time_spent
    visitors.average(:visit_time) # Or whatever      
  end
end
Mike
There are three issues: 1) The sql to find the stats is really complex (not just a count/average). Could i use a find_by_sql? 2) I want to return the Restaurants sorted by the complex SQL, is that possible this way? 3) the :stat_value column does not exist in the restaurant table, how will those get set?
Lee
Mike has 90% of your answer. #find_by_sql will return a Restaurant instance, and ActiveRecord magic will kick in to allow any columns returned by SQL to be accessible from the view. Restaurant.find_by_sql("SELECT id, COUNT(visitors.*) visitor_count FROM restaurants ORDER BY visitor_count DESC").
François Beausoleil
@François: This is untrue. This solution does not provide a visitors_count column, just methods that calculate the statistics. So they cannot be sorted via SQL.
EmFi
+1  A: 

Given that you want to sort your searches on statistics, adding counter caches to the Restaurant model looks like your best options.

In the case of total visitors this is simple. In the case of total time_spent, it will be a little more complex, but still not unmanageable. If you're looking for an average then things get a little more complicated.

Here's the code required to add counter caches to your restraurant model. Notice that most of new model code is in the visitor's model.

Add new columns to restaurant via migration:

class AddCounterCaches < ActiveRecord::Miration
  def self.up
    add_column :restaurants, :visitors_count, :integer, :default => 0
    add_column :restaurants, :total_time_spent, :integer, :default => 0
    Restaurant.reset_column_information
    Restaurant.find(:all).each do |r|
      count = r.visitors.length
      total = r.visitors.inject(0) {|sum, v| sum + v.time_spent}
      average = count == 0 ? 0 : total/count
      r.update_counters r.id, :visitors_count => count
       :total_time_spent => total, :average_time_spent => average
    end
  end

  def self.down
    remove_column :restaurants, :visitors_count        
    remove_column :restaurants, :total_time_spent
  end
end

Update visitor model to update counter caches

class Vistor < ActiveRecord::Base
  belongs_to :restaurant, :counter_cache => true 

  after_save :update_restaurant_time_spent, 
    :if => Proc.new {|v| v.changed.include?("time_spent")}

  def :update_restaurant_time_spent
    difference = changes["time_spent"].last - changes["time_spent"].first
    Restaurant.update_counters(restaurant_id, :total_time_spent => difference)       
    restaurant.reload   
    avg = restaurant.visitors_count == 0 ? 
      0 : restaurant.total_time_spent / restaurant.visitors_count
    restaurant.update_attribute(:average_time_spent, avg)
  end     
end

N.B. Code hasn't been tested, so it may contain minor errors.

Now you can sort by these columns, create named scopes that include them or use them in your methods.

EmFi