ActiveRecord で関連レコードの size をプリロードする

テーブルの一覧表示で関連レコードの数を表示したい場合、
View でそのまま records[i].subrecords.size などとして繰り返し参照すると SELECT count(*) ... というようなカウント SQL が大量に実行されてしまう。

以下を利用するとカウント SQL の実行を1回にできる。

使い方

records = Record.find(:all, :include_count => :subrecords)
records[0].subrecords.size # カウント SQL が実行されない

lib/active_record_include_count.rb

require 'active_record'

class ActiveRecord::Base
  VALID_FIND_OPTIONS << :include_count

  def self.find(*args)
    options = args.extract_options!
    validate_find_options(options)
    set_readonly_option!(options)

    records = case args.first
      when :first then find_initial(options)
      when :last  then find_last(options)
      when :all   then find_every(options)
      else             find_from_ids(args, options)
    end

    if options.has_key?(:include_count)
      Array(options[:include_count]).each do |association|
        preload_sizes(records, association)
      end
    end
    records
  end

private

  def self.preload_sizes(records, association)
    class_to_reflection = {}
    records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
      raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
      sizes = {}
      ids = records.map {|record| record.quoted_id}.join(',')
      reflection.klass.count(:conditions => "#{reflection.primary_key_name} IN (#{ids})",
                             :group => reflection.primary_key_name
                            ).map {|record| sizes[record[0]] = record[1]}
      records.each do |record|
        unless sizes[record.id]
          record.send(association).loaded
        else
          record["#{reflection.name}_count"] = sizes[record.id]
        end
      end
    end
  end
end

config/initializers/active_record.rb

require 'active_record_include_count'