Search code examples
ruby-on-railsassociationscountertransitivity

transitive rails associations and magical count


In Rails, to automatically count associations, you do:

class Script
  has_many :chapters
end
class Chapter
  belongs_to :script
end

and you add a chapters_count column into the Script model.

Now, what if you want to count the number of paragraphs in a Script without having a script_id key in the paragraph model ?

class Script
  has_many :chapters
  has_many :paragraphs # not complete
end
class Chapter
  has_many :paragraphs
  belongs_to :script
end
class Paragraph
  belongs_to :chapter
end

How do you automatically associate script to paragraph and count them using the automatic count of Rails ?


Solution

  • You're on the right track. But first you've got to address a small error. Rails won't update a counter cache unless you instruct it to.

    class Chapter
      belongs_to :script, :counter_cache => true
    end
    

    Will automatically update @script.chapter_count before creation and after destruction of all associated Chapters.

    Unfortunately things aren't so simply when dealing :through relationships. You will need to update the associated script's paragraph counter through callbacks in the Paragraph model.

    N.B.: The following assumes you want to keep a paragraph counter in Chapter as well.

    Start by applying the same theory to the Chapter model, and a paragraphs count column to the Script table.

    class PrepareForCounterCache < ActiveRecord::Migration
      def self.up
        add_column :scripts, :paragraphs_count, :integer, :default => 0
        add_column :chapters, :paragraphs_count, :integer, :default => 0
    
        Chapter.reset_column_information
        Script.reset_column_information
    
        Chapter.find(:all).each do |c|
          paragraphs_count = c.paragraphs.length
          Chapter.update_counters c.id, :paragraphs_count => paragraphs_count
          Script.update_counters c.script_id, :paragraphs_count => paragraphs_count
        end
      end
      def self.down
        remove_column :scripts, :paragraphs_count
        remove_column :chapters, :paragraphs_count
      end
    end 
    

    Now to set up the relationships:

    class Script
      has_many: chapters
      has_many: paragraphs, :through => :chapters
    end
    
    class Chapter
      has_many: paragraphs
      belongs_to :script, :counter_cache => true
    end
    
    class Paragraph
      belongs_to :chapter, :counter_cache => true
    end
    

    All that's left is to tell Paragraph to update the paragraph counters in script as a callback.

    class Paragraph < ActiveRecord::Base
      belongs_to :chapter, :counter_cache => true
      before_save :increment_script_paragraph_count
      after_destroy, :decrement_script_paragraph_count
    
      protected
      def increment_script_paragraph_count
        Script.update_counters chapter.script_id, :paragaraphs_count => 1
      end
      def decrement_script_paragraph_count
        Script.update_counters chapter.script_id, :paragaraphs_count => -1
      end
    end