Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Rails, how to count the number of associated records in a collection?

I have a model named Awardunit. Awardunit have many Awardleaders. One Award unit can have one or many Award leaders.

If I get all the record or search and get a collection of records to a variable named awardunits how can I count the number of Awardleaders in all the units in this collection?

Here's what I did :

 @leaders = 0
 @awardunits.each do |unit|
      @leaders = @leaders + unit.awardleaders.size
 end

Again to count the disabled leaders I use this :

  @disabledleaders = 0
  @awardunits.each do |unit|
     @disabledleaders = @disabledleaders + unit.awardleaders.where(disabled: true).size
  end

If I use this, it will have to go through all the records every time the page loads. Isn't there a better way of doing this?

like image 479
THpubs Avatar asked Dec 20 '22 20:12

THpubs


2 Answers

You can make counting the associations cheap by adding a counter cache:

class AwardUnit << ActiveRecord::Base
  has_many :award_units
end

class AwardLeader << ActiveRecord::Base
  belongs_to :award_unit, counter_cache: true
end

Now add a new column called award_leaders_count to your AwardUnit table in a new migration:

  def change
    add_column :award_units, :award_leaders_count, :integer, default: 0

    AwardUnit.all.each do |unit|
      AwardUnit.reset_counters(unit.id, :award_leaders)
    end
  end

Rails will now automatically cache the number of award_leaders for every AwardUnit and @my_award_unit.award_leaders.count will give you the count without running another database query.

By default, Rails counter_cache only works for all award_leaders. If you need to count only those award_leaders that have a condition, you will have to add your own counter_cache:

class AwardUnit << ActiveRecord::Base
  has_many :award_units
end

class AwardLeader << ActiveRecord::Base
  belongs_to :award_unit

  scope :disabled, -> { where(disabled: true) }

  after_save :update_counter_cache
  after_destroy :update_counter_cache

  def update_counter_cache
    award_unit.update_attribute(:disabled_award_units_count, award_unit.award_leaders.disabled.count)
  end
end

migration:

  def change
    add_column :award_units, :disabled_award_leaders_count, :integer, default: 0

    AwardUnit.all.each do |unit|
      unit.update_attribute(:disabled_award_units_count, unit.award_leaders.disabled.count)
    end
  end

Now, when you have an array of AwardUnits, getting their combined count of disabled award leaders is as simple as

@award_units = AwardUnit.limit(5).to_a # or a similar query
@award_units.inject(0){|sum,unit| sum + unit.disabled_award_leaders_count }
like image 103
janfoeh Avatar answered May 18 '23 16:05

janfoeh


You can eager load Awardleaders when fetching Awardunits, so you don't have to execute a count query for every Awardunit, like this:

@awardunits = Awardunit.includes(:awardleaders).where('awardleaders.disabled = ?', true) # the rest of the query

Or, you can query the count directly like this:

@leaders = Awardleader.where('awardunit_id IN (?)', @awardunits.map(&:id)).where(:disabled => true).count
like image 44
Ahmad Sherif Avatar answered May 18 '23 14:05

Ahmad Sherif