Understanding of ActiveRecord Aggregations
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.
Comments