ruby-on-railsrubyactiverecordactive-record-query

How to group and count a model by a has_one association's attribute?


I have a station model which can have multiple devices whereas each device belongs_to a station. Each station has an address, this is a polymorphic model:

/app/models/station.rb

class Station < ApplicationRecord
  has_one :address, as: :addressable, dependent: :destroy
  has_many :devices
end

/app/models/device.rb

class Device < ApplicationRecord
  belongs_to :station
  has_one :address, through: :station
end

/app/models/address.rb

# @attr [String] city City name of address
class Address < ApplicationRecord
  belongs_to :addressable, polymorphic: true
end

Now I need two sets of data for my charts:

What works is getting the number of stations by city:

def stations_by_city
  # collect data
  tmp_result = Address.where(addressable_type: 'Station').group(:city).count
  # sort and return top five cities
  result = Hash[tmp_result.sort_by { |_k, v| -v }[0..4]]
  # result = {"City-1"=>17, "City-2"=>14, "City-3"=>14, "City-4"=>12, "City-5=>11} 
end

Doing the same for devices by city isn't working as expected. By now I do it this way:

def devices_by_city
  stations = Station.all.includes(:address)
  tmp_result = {}
  # for each unique city
  list_of_cities.uniq.each do |city|
   number_of_devices = 0
   # check all stations
   stations.each do |station|         
     address = station.address
     # check if station is in city
     if address.city == city
       # and add to counter
       number_of_devices += station.devices.count
     end
    end
    # append to resulting hash
    tmp_result[city] = number_of_devices
  end
  result = Hash[tmp_result.sort_by { |_k, v| -v }[0..4]]
end

def list_of_cities
  cities = []
  stations = Station.all.includes(:address)
  stations.each do |station|
    address = station.address
    cities << address.city
  end
    cities
  end
end

I have duplicate database lookups and it's generally quite ugly. How can I write this query in a better way? Tried various [.joins, .where, .group]-combinations but non of them worked. Adding through: :station to the device model helped at other places but didn't simplify my problem ...

Update from answer

# start join from station model
tmp_result = Station.joins(:address, :devices).group(:city).count

# start join from device model
tmp_result = Device.joins(station: :address).group(:city).count

Starting the join from device model is the fastest one:

Timing for old query
  0.530000   0.050000   0.580000 (  0.668664)
Timing for query starting from station model
  0.020000   0.000000   0.020000 (  0.024881)
Timing for query starting from device model
  0.010000   0.000000   0.010000 (  0.009616)

Solution

  • You could a joins between Station, Address and Device models and group_by the results by the city and then apply count:

    def devices_by_city_updated
      temp_result = Station.joins(:address, :devices).group(:city).count
      result = Hash[tmp_result.sort_by { |_k, v| -v }[0..4]]
    end
    

    This query will do a single database lookup to get all the information.

    You could start the join from the Device model as well. But you have to join nested associations for this to work:

    def self.devices_by_city_another
      tmp_result = Device.joins(station: :address).group(:city).count
      result = Hash[tmp_result.sort_by { |_k, v| -v }[0..4]]
    end
    

    You can check for more information in the docs