如何在 rails 中缓存计算的列?

Posted

技术标签:

【中文标题】如何在 rails 中缓存计算的列?【英文标题】:How can I cache a calculated column in rails? 【发布时间】:2010-09-15 21:59:27 【问题描述】:

我有一棵活动记录对象树,例如:

class Part < ActiveRecord::Base
  has_many :sub_parts, :class_name => "Part"

  def complicated_calculation
    if sub_parts.size > 0
      return self.sub_parts.inject(0) |sum, current| sum + current.complicated_calculation 
    else
      sleep(1)
      return rand(10000)
    end
  end

end

每次重新计算 complex_calculation 的成本太高。所以,我需要一种缓存值的方法。但是,如果任何部分发生更改,则需要使其缓存以及其父级和祖父级的缓存等失效。

作为一个粗略的草稿,我创建了一个列来将缓存的计算保存在“parts”表中,但这闻起来有点烂。似乎应该有一种更简洁的方法来缓存计算值,而不是将它们塞到“真实”列旁边。

【问题讨论】:

【参考方案1】:

我建议使用关联回调。

class Part < ActiveRecord::Base
  has_many :sub_parts,
    :class_name => "Part",
    :after_add => :count_sub_parts,
    :after_remove => :count_sub_parts

  private

  def count_sub_parts
    update_attribute(:sub_part_count, calculate_sub_part_count)
  end

  def calculate_sub_part_count
    # perform the actual calculation here
  end
end

又好又简单 =)

【讨论】:

我猜这不会处理您从另一个方向(通过 has_many)创建 sub_part 的情况,如下所示: Part.create(: parent_part => the_parent_part)。我可能会在 Part 上添加一个 after_create 回调,以确保在这种情况下也会触发 count_sub_parts ... 刚刚在 Rails 4 中做了一点测试并验证,当您直接创建子记录(sub_part)时,这些 after_add 和 after_remove 钩子不会触发【参考方案2】:

    您可以将实际缓存的值填充到 Rails 缓存中(如果需要分发,请使用 memcached)。

    难点是缓存到期,但缓存到期并不常见,对吧?在这种情况下,我们可以依次循环遍历每个父对象并删除其缓存。我在您的类中添加了一些 ActiveRecord 魔法,以使获取父对象本身变得简单——您甚至不需要接触您的数据库。记得在你的代码中适当地调用Part.sweep_complicated_cache(some_part)——你可以把它放在回调等中,但我不能为你添加它,因为我不明白complicated_calculation什么时候改变。

    class Part < ActiveRecord::Base
      has_many :sub_parts, :class_name => "Part"
      belongs_to :parent_part, :class_name => "Part", :foreign_key => :part_id
    
      @@MAX_PART_NESTING = 25 #pick any sanity-saving value
    
      def complicated_calculation (...)
        if cache.contains? [id, :complicated_calculation]
          cache[ [id, :complicated_calculation] ]
        else
          cache[ [id, :complicated_calculation] ] = complicated_calculation_helper (...)
        end
      end
    
      def complicated_calculation_helper
        #your implementation goes here
      end
    
      def Part.sweep_complicated_cache(start_part)
        level = 1  # keep track to prevent infinite loop in event there is a cycle in parts
        current_part = self
    
        cache[ [current_part.id, :complicated_calculation] ].delete
        while ( (level <= 1 < @@MAX_PART_NESTING) && (current_part.parent_part)) 
         current_part = current_part.parent_part)
         cache[ [current_part.id, :complicated_calculation] ].delete
        end
      end
    end
    

【讨论】:

【参考方案3】:

有一个类似于计数器缓存的字段。例如: order_items_amount 并将其作为缓存的计算字段。

使用 after_save 过滤器重新计算任何可以修改该值的字段。 (包括记录本身)

编辑:这基本上就是你现在所拥有的。除非您想将缓存的计算字段存储在另一个表中,否则我不知道有什么更简洁的解决方案。

【讨论】:

【参考方案4】:

使用 before_save 或 ActiveRecord Observer 是确保缓存值是最新的方法。我会使用 before_save 然后检查您在计算中使用的值是否实际发生了变化。这样您就不必在不需要时更新缓存。 将值存储在数据库中将允许您缓存多个请求的计算。另一种选择是将值存储在内存缓存中。您可以为该值创建一个特殊的访问器和设置器,以检查内存缓存并在需要时对其进行更新。 另一个想法:是否存在您将更改其中一个模型中的值并需要在保存之前更新计算的情况?在这种情况下,每当您更新模型中的任何计算值时,您都需要弄脏缓存值,而不是使用 before_save。

【讨论】:

【参考方案5】:

我发现有时有充分的理由对数据库中的信息进行非规范化处理。我正在开发的应用程序中有类似的东西,只要集合发生变化,我就会重新计算该字段。

它不使用缓存,而是在数据库中存储最新的数字。

【讨论】:

以上是关于如何在 rails 中缓存计算的列?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 中缓存模型对象?

在 Rails 6 中更新底层模型时如何使动作缓存过期?

你如何更改rails中的列数据类型?

如何让列自动计算?导轨

如何在 ruby​​ on rails 中使每个 user_id 的列数据唯一

Rails:旧数据与新数据不匹配时如何更新片段缓存