Understanding of ActiveRecord Aggregations

# rails # ruby
22 January 2025
193 views

It's quite common thing we all bump in while working with Models in Rails apps. Imagine that we have Event model that represents some meetup or conference with following fields:

  • city

  • address

  • longitude

  • latitude

  • ...

All these fields are stored in the database in events table. And it would be really nice to work with all these 4 fields as an object. Let's call it Location. Why would it be nice? Because having an abstraction gives us flexibility. For example we can compare 2 locations with each other or check that one location is close enough to other one. Furthermore this logic could be encapsulated in Location class and could be tested separately. Lets take a look at examples below

# app/models/event.rb
#
# == Schema Information
#
# Table name: events
#
#  id         :bigint           not null, primary key
#  address    :string
#  city       :string
#  latitude   :float
#  longitude  :float
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Event < ApplicationRecord
  composed_of :location, mapping: {address: :address, latitude: :latitude, longitude: :longitude }
end

With composed_of we can create an aggregation of this 4 fields to the value object Location.

# app/models/location.rb

require "math"

class Location
  attr_reader :city, :address, :latitude, :longitude

  EARTH_RADIUS_KM = 6371

  def initialize(city:, address:, latitude:, longitude:)
    @city = city
    @address = address
    @latitude = latitude
    @longitude = longitude
  end

  def ==(other)
    latitude == other.latitude && longitude == other.longitude
  end

  def close_to?(other, max_distance_km = 1)
    distance = haversine_distance(latitude, longitude, other.latitude, other.longitude)
    distance <= max_distance_km
  end

  private

  def haversine_distance(lat1, lon1, lat2, lon2)
    # Convert degrees to radians
    lat1_rad, lon1_rad = to_radians(lat1), to_radians(lon1)
    lat2_rad, lon2_rad = to_radians(lat2), to_radians(lon2)

    # Haversine formula
    delta_lat = lat2_rad - lat1_rad
    delta_lon = lon2_rad - lon1_rad

    a = Math.sin(delta_lat / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(delta_lon / 2)**2
    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))

    # Distance in kilometers
    EARTH_RADIUS_KM * c
  end

  def to_radians(degrees)
    degrees * Math::PI / 180
  end
end

Due to composed_of macro we can compare two Events by theirs location

event1 = Event.new(city: "Warsaw", address: "Marszałkowska 1", latitude: 52.22977, longitude: 21.01178)
event2 = Event.new(city: "Warsaw", address: "Marszałkowska 5", latitude: 52.22978, longitude: 21.01179)

event1.location.close_to?(event2.location) # => true

Also Rails gives us the ability to query records using Location object in a where clause

location = Location.new(city: "Warsaw", address: "Marszałkowska 1", latitude: 52.22977, longitude: 21.01178)

Event.where(location: location) # => returns Events where fields match with Location fields

For more examples take a look at the documentation page of Active Record Aggregations.