16

次のようなアクティブなレコードオブジェクトのツリーがあります。

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を再計算するにはコストがかかりすぎます。したがって、値をキャッシュする方法が必要です。ただし、一部が変更された場合は、そのキャッシュとその親、祖父母などのキャッシュを無効にする必要があります。

大まかなドラフトとして、「パーツ」テーブルにキャッシュされた計算を保持する列を作成しましたが、これは少し腐ったにおいがします。計算された値を「実際の」列の横に詰め込むことなくキャッシュするためのよりクリーンな方法があるはずです。

4

5 に答える 5

28

関連コールバックを使用することをお勧めします。

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

素敵で簡単 =)

于 2008-10-08T07:28:41.260 に答える
8
  1. 実際にキャッシュされた値を Rails キャッシュに詰め込むことができます (分散する必要がある場合は memcached を使用してください)。

  2. 難しいのはキャッシュの有効期限ですが、キャッシュの有効期限は一般的ではありませんよね? その場合、各親オブジェクトを順番にループして、そのキャッシュも消去できます。クラスに 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
    
于 2008-10-08T06:01:48.563 に答える
2

カウンターキャッシュに似たフィールドがあります。例:order_items_amountは、キャッシュされた計算フィールドになります。

after_saveフィルターを使用して、その値を変更できるものすべてのフィールドを再計算します。(レコード自体を含む)

編集:これは基本的にあなたが今持っているものです。キャッシュされた計算フィールドを別のテーブルに格納したい場合を除いて、よりクリーンなソリューションはわかりません。

于 2008-10-08T01:42:40.037 に答える
2

before_save または ActiveRecord Observer を使用して、キャッシュされた値が最新であることを確認します。before_save を使用してから、計算で使用する値が実際に変更されたかどうかを確認します。そうすれば、必要がなければキャッシュを更新する必要はありません。
値をデータベースに保存すると、複数のリクエストにわたって計算をキャッシュできます。このための別のオプションは、値を memcache に保存することです。memcache をチェックして必要に応じて更新できる、その値の特別なアクセサーとセッターを作成できます。
別の考え: モデルの 1 つの値を変更し、保存する前に計算を更新する必要がある場合はありますか? その場合、before_save ではなく、モデル内の計算値を更新するたびに、キャッシュ値をダーティにする必要があります。

于 2008-10-08T03:20:49.230 に答える
1

データベース内の情報を非正規化する正当な理由がある場合があることがわかりました。私が取り組んでいるアプリに似たようなものがあり、コレクションが変更されるたびにそのフィールドを再計算します。

キャッシュを使用せず、最新の図をデータベースに保存します。

于 2008-10-13T16:57:22.640 に答える