Как обновить counter_cache при обновлении модели? - PullRequest
6 голосов
/ 22 апреля 2011

У меня есть простые отношения:

class Item
  belongs_to :container, :counter_cache => true
end

class Container
  has_many :items
end

Допустим, у меня есть два контейнера.Я создаю элемент и связываю его с первым контейнером.Счетчик увеличивается.

Затем я решаю связать его с другим контейнером.Как обновить столбец items_count в обоих контейнерах?

Я нашел возможное решение в http://railsforum.com/viewtopic.php?id=39285 ... однако я новичок и не понимаю его.Это единственный способ сделать это?

Ответы [ 8 ]

3 голосов
/ 20 июля 2012

Немного изменил его для обработки имен пользовательских кеш-счетчиков (не забудьте добавить after_update :fix_updated_counter к моделям, использующим counter_cache)

module FixUpdateCounters

  def fix_updated_counters
    self.changes.each { |key, (old_value, new_value)|
      # key should match /master_files_id/ or /bibls_id/
      # value should be an array ['old value', 'new value']
      if key =~ /_id/
        changed_class = key.sub /_id$/, ''
        association   = self.association changed_class.to_sym

        case option = association.options[ :counter_cache ]
        when TrueClass
          counter_name = "#{self.class.name.tableize}_count"
        when Symbol
          counter_name = option.to_s
        end

        next unless counter_name

        association.klass.decrement_counter(counter_name, old_value) if old_value
        association.klass.increment_counter(counter_name, new_value) if new_value
      end
    }   end end

ActiveRecord::Base.send(:include, FixUpdateCounters)
3 голосов
/ 22 апреля 2011

Должно работать автоматически.Когда вы обновляете items.container_id, он уменьшает счетчик старого контейнера и увеличивает новый.Но если это не работает - это странно.Вы можете попробовать этот обратный вызов:

class Item
  belongs_to :container, :counter_cache => true
  before_save :update_counters

  private
  def update_counters
    new_container = Container.find self.container_id
    old_container = Container.find self.container_id_was
    new_container.increament(:items_count)
    old_container.decreament(:items_count)
  end
end

UPD

Чтобы продемонстрировать нативное поведение:

container1 = Container.create :title => "container 1"
#=> #<Container title: "container 1", :items_count: nil>
container2 = Container.create :title => "container 2"
#=> #<Container title: "container 2", :items_count: nil>
item = container1.items.create(:title => "item 1")
Container.first
#=> #<Container title: "container 1", :items_count: 1>
Container.last
#=> #<Container title: "container 1", :items_count: nil>
item.container = Container.last
item.save
Container.first
#=> #<Container title: "container 1", :items_count: 0>
Container.last
#=> #<Container title: "container 1", :items_count: 1>

Так что он должен работать без какого-либо взлома.Из коробки.

2 голосов
/ 28 октября 2012

вот подход, который хорошо работает для меня в подобных ситуациях

class Item < ActiveRecord::Base

    after_update :update_items_counts, if: Proc.new { |item| item.collection_id_changed? }

private

    # update the counter_cache column on the changed collections
    def update_items_counts

        self.collection_id_change.each do |id|
            Collection.reset_counters id, :items
        end

    end

end

дополнительная информация о грязном объектном модуле http://api.rubyonrails.org/classes/ActiveModel/Dirty.html и старое видео о них http://railscasts.com/episodes/109-tracking-attribute-changes и документацияна reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters

2 голосов
/ 11 октября 2011

Для рельсов 3.1 пользователей. С рельсами 3.1 ответ не работает. Следующее работает для меня.

  private
    def update_counters
      new_container = Container.find self.container_id
      Container.increment_counter(:items_count, new_container)
      if self.container_id_was.present?
        old_container = Container.find self.container_id_was
        Container.decrement_counter(:items_count, old_container)
      end
    end
1 голос
/ 12 апреля 2013

Извините, у меня недостаточно репутации, чтобы комментировать ответы.
Что касается fl00r, я могу увидеть проблему, если есть ошибка и сохранить возвращаемое значение «false», счетчик уже обновлен, но он не должен был обновляться. Поэтому мне интересно, является ли "after_update: update_counters" более подходящим.

Ответ Керли работает, но если вы в моем случае, будьте осторожны, потому что он проверит все столбцы с "_id". В моем случае это автоматическое обновление поля, которое я не хочу обновлять.

Вот еще одно предложение (почти похожее на Satish):

def update_counters
   if container_id_changed?
     Container.increment_counter(:items_count, container_id) unless container_id.nil?
     Container.decrement_counter(:items_count, container_id_was) unless container_id_was.nil?
   end
end
1 голос
/ 18 июня 2012

Здесь исправление @Curley для работы с моделями пространства имен.

module FixUpdateCounters

  def fix_updated_counters
    self.changes.each {|key, value|
      # key should match /master_files_id/ or /bibls_id/
      # value should be an array ['old value', 'new value']
      if key =~ /_id/
        changed_class = key.sub(/_id/, '')

        # Get real class of changed attribute, so work both with namespaced/normal models
        klass = self.association(changed_class.to_sym).klass

        # Namespaced model return a slash, split it.
        unless (counter_name = "#{self.class.name.underscore.pluralize.split("/")[1]}_count".to_sym)
          counter_name = "#{self.class.name.underscore.pluralize}_count".to_sym
        end

        klass.decrement_counter(counter_name, value[0]) unless value[0] == nil
        klass.increment_counter(counter_name, value[1]) unless value[1] == nil
      end
    }
  end 
end

ActiveRecord::Base.send(:include, FixUpdateCounters)
1 голос
/ 02 мая 2012

Недавно я столкнулся с этой же проблемой (Rails 3.2.3). Похоже, что это еще не исправлено, поэтому я должен был пойти дальше и исправить. Ниже показано, как я изменил ActiveRecord :: Base и использую обратный вызов after_update для синхронизации моих counter_caches.

Расширение ActiveRecord :: Base

Создайте новый файл lib/fix_counters_update.rb со следующим:

module FixUpdateCounters

  def fix_updated_counters
    self.changes.each {|key, value|
      # key should match /master_files_id/ or /bibls_id/
      # value should be an array ['old value', 'new value']
      if key =~ /_id/
        changed_class = key.sub(/_id/, '')
        changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
        changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
      end
    }
  end 
end

ActiveRecord::Base.send(:include, FixUpdateCounters)

В приведенном выше коде используется метод ActiveModel :: Dirty changes, который возвращает хеш, содержащий измененный атрибут и массив как старого, так и нового значения. Протестировав атрибут, чтобы определить, является ли это отношением (т.е. оканчивается на / _id /), вы можете условно определить, нужно ли запускать decrement_counter и / или increment_counter. Необходимо проверить наличие nil в массиве, в противном случае возникнут ошибки.

Добавить к инициализаторам

Создайте новый файл config/initializers/active_record_extensions.rb со следующим:

require 'fix_update_counters'

Добавить к моделям

Для каждой модели, которую вы хотите обновить кэши счетчиков, добавьте обратный вызов:

class Comment < ActiveRecord::Base
  after_update :fix_updated_counters
  ....
end
1 голос
/ 11 октября 2011

Обновления до @ fl00r Ответ

class Container
  has_many :items_count
end

class Item
  belongs_to :container, :counter_cache => true
  after_update :update_counters

  private

 def update_counters
   if container_id_changed?
     Container.increment_counter(:items_count, container_id)
     Container.decrement_counter(:items_count, container_id_was)
   end

   # other counters if any
   ...
   ...

 end

end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...