【问题标题】:Rails Model With Aggregrate Data (not backed by a table)具有聚合数据的 Rails 模型(不受表支持)
【发布时间】:2025-12-04 19:05:04
【问题描述】:

我想在 Rails 中创建一个与数据库中的表不相关的模型。相反,模型应该动态地提取关于其他模型的聚合数据。

例子:

我有一个餐厅模型存储在数据库的餐厅表中。我想有一个 RestaurantStats 模型,我可以在其中运行 RestaurantStats.find_total_visitors 或 RestaurantStats.find_time_spent 等......它会返回一组 RestaurantStats 模型,每个模型包含:

[:restaurant_id, :stat_value]

显然,在每个 find... 方法中,stat_value 的含义有所不同(对于 find_time_spent,它将花费秒数,对于 find_total_visitors,它将是访问者的数量)。这个想法将是按花费的时间或总访客数返回前 100 家餐厅。

到目前为止,我正在创建一个模型(不是从 ActiveRecord 继承的)

class RestaurantStats 
   attr_reader :restaurant_id
   attr_reader :stat_value

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

问题是如何以 rails y 方式定义 find_total_visitors、find_time_spent 函数,以便填充 restaurant_id、stat_value 字段?

【问题讨论】:

    标签: ruby-on-rails activerecord model


    【解决方案1】:

    您确定不只是希望这些方法成为餐厅的方法吗?

    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
    

    【讨论】:

    • 存在三个问题: 1) 查找统计信息的 sql 非常复杂(不仅仅是计数/平均值)。我可以使用 find_by_sql 吗? 2) 我想返回按复杂 SQL 排序的餐厅,这样可以吗? 3) :stat_value 列在餐厅表中不存在,如何设置?
    • 迈克有你 90% 的答案。 #find_by_sql 将返回一个 Restaurant 实例,并且 ActiveRecord 魔法将启动以允许从视图访问 SQL 返回的任何列。 Restaurant.find_by_sql("SELECT id, COUNT(visitors.*) visitor_count FROM restaurant ORDER BY visitor_count DESC").
    • @François:这是不真实的。此解决方案不提供 visitor_count 列,仅提供计算统计信息的方法。所以它们不能通过 SQL 排序。
    【解决方案2】:

    使用 self.(fieldname) 设置值,然后保存这些值(在运行查找或构建之后)。

    【讨论】:

      【解决方案3】:

      鉴于您希望根据统计信息对搜索进行排序,将计数器缓存添加到 Restaurant 模型似乎是您的最佳选择。

      在访问者总数的情况下,这很简单。在总 time_spent 的情况下,它会稍微复杂一些,但仍然不是无法管理的。如果您正在寻找平均值,那么事情会变得更加复杂。

      以下是将计数器缓存添加到餐厅模型所需的代码。请注意,大部分新模型代码都在访问者的模型中。

      通过迁移将新列添加到Restaurant

      class AddCounterCaches < ActiveRecord::Migration
        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
      

      更新访问者模型以更新计数器缓存

      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 
      

      注意代码未经测试,因此可能包含小错误。

      现在您可以按这些列排序,创建包含它们的命名范围或在您的方法中使用它们。

      【讨论】: