2011-04-22 60 views
5

Tôi có một mối quan hệ đơn giản:Làm thế nào để cập nhật counter_cache khi cập nhật một mô hình?

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

class Container 
    has_many :items 
end 

Giả sử tôi có hai thùng chứa. Tôi tạo một mục và liên kết nó với hộp chứa đầu tiên. Bộ đếm được tăng lên.

Sau đó, tôi quyết định liên kết nó với vùng chứa khác thay thế. Cách cập nhật cột items_count của cả hai vùng chứa?

Tôi tìm thấy giải pháp có thể tại http://railsforum.com/viewtopic.php?id=39285 .. tuy nhiên tôi là người mới bắt đầu và tôi không hiểu nó. Đây có phải là cách duy nhất để làm điều đó không?

Trả lời

3

Nó sẽ hoạt động tự động. Khi bạn đang cập nhật items.container_id, nó sẽ làm hỏng bộ đếm cũ của thùng chứa và hình ảnh mới. Nhưng nếu nó không hoạt động - nó là lạ. Bạn có thể thử gọi lại này:

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

Để chứng minh hành vi tự nhiên:

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> 

Vì vậy, nó nên làm việc mà không cần bất kỳ hack. Từ chiếc hộp.

2

Dành cho đường ray 3.1 người dùng. Với đường ray 3.1, câu trả lời không hoạt động. Tác phẩm sau đây dành cho tôi.

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

Cập nhật cho @ fl00r trả lời

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 
+0

Điều đó có thể sẽ ném ra một lỗi nếu container_id/container_id_was là con số không. – Zequez

1

Gần đây tôi đi qua cùng một vấn đề này (Rails 3.2.3). Có vẻ như nó chưa được sửa, nên tôi phải tiếp tục và sửa chữa. Dưới đây là cách tôi đã sửa đổi ActiveRecord :: Base và sử dụng gọi lại after_update để giữ cho counter_caches của tôi được đồng bộ.

Mở rộng ActiveRecord :: Base

Tạo một file mới lib/fix_counters_update.rb như sau:

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) 

Đoạn mã trên sử dụng phương pháp ActiveModel::Dirtychanges mà trả về một băm chứa các thuộc tính thay đổi và một mảng của cả giá trị cũ và giá trị mới. Bằng cách kiểm tra thuộc tính để xem liệu đó có phải là mối quan hệ (tức là kết thúc bằng/_id /) hay không, bạn có thể xác định có điều kiện xem decrement_counter và/hoặc increment_counter có cần chạy hay không. Đó là essnetial để kiểm tra sự hiện diện của nil trong mảng, nếu không sẽ xảy ra lỗi.

Thêm vào Initializers

Tạo một file mới config/initializers/active_record_extensions.rb như sau:

require 'fix_update_counters'

Thêm vào mô hình

Đối với mỗi mô hình mà bạn muốn lưu trữ truy cập được cập nhật thêm gọi lại:

class Comment < ActiveRecord::Base 
    after_update :fix_updated_counters 
    .... 
end 
1

Tại đây, @Curley khắc phục để làm việc với các mô hình không gian tên.

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) 
3

Modified nó một chút để xử lý tên bộ nhớ cache tùy chỉnh truy cập (Đừng quên để thêm after_update :fix_updated_counter các mô hình sử dụng 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) 
+0

Câu trả lời này là năng động nhất và sẽ là tốt nhất cho các dự án có rất nhiều bộ đệm truy cập bao gồm một số tên tùy chỉnh. Đối với Martin, câu trả lời được chấp nhận có thể dễ dàng nhất. –

2

đây là một cách tiếp cận hoạt động tốt cho tôi trong tương tự các tình huống

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 

thông tin bổ sung về mô-đun đối tượng bẩn http://api.rubyonrails.org/classes/ActiveModel/Dirty.html và video cũ về chúng http://railscasts.com/episodes/109-tracking-attribute-changes và tài liệu về reset_counters http://apidock.com/rails/v3.2.8/ActiveRecord/CounterCache/reset_counters

1

Xin lỗi tôi không có đủ danh tiếng để nhận xét câu trả lời.
Giới thiệu về fl00r, tôi có thể gặp sự cố nếu có lỗi và lưu lại "sai", bộ đếm đã được cập nhật nhưng nó chưa được cập nhật. Vì vậy, tôi tự hỏi liệu "after_update: update_counters" có phù hợp hơn không.

Câu trả lời của Curley có hiệu quả nhưng nếu bạn đang ở trong trường hợp của tôi, hãy cẩn thận vì nó sẽ kiểm tra tất cả các cột bằng "_id". Trong trường hợp của tôi, nó sẽ tự động cập nhật một trường mà tôi không muốn được cập nhật.

Dưới đây là một gợi ý (gần như tương tự như 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 
Các vấn đề liên quan