delegate_cached is a Ruby gem to easily cache delegated attributes on an ActiveRecord model, and have the cache updated when the delegated value changes.
One regret I have from my Intridea days is that I was so busy running the business that I rarely had time to contribute in meaningful ways to open-source projects. Sure, I pitched in a commit here or there on various projects, but largely my contributions involved clearing the schedule of our team’s engineers enough so they could work on their own open-source projects.
So, it is with great pleasure that, at long last, I release my first
open-source Ruby gem: delegate_cached
The Problem
As with most open-source contributions, this gem came about in response to a pain point I was having on an existing project. After installing the bullet gem and tracking down a lot of N+1 queries, I found myself frequently having to include a rarely-touched model.
Our Company model references an IndustryCompanyList (names have been changed
to protect the innocent), which is a occasionally-updated “official” list of
companies from an external industry source. OfficialCompanyList has a name
attribute we would like to display as the Company name, so we delegate to
OfficialCompanyList as below.
class Company < ApplicationRecord
belongs_to :industry_company_list
delegate :name, to: :industry_company_list, prefix: false
endThis works well. With any instance of Company, we can call company.name and
we get the value through the delegated association.
IndustryCompanyList must then, however, be included every time company.name
is referenced to avoid the N+1 query issue. Storing the value from
IndustryCompanyList in a column on Company would prevent this issue, but
there would need to be a process to prevent the data from going stale when
IndustryCompanyList values are updated. Enter, delegate_cached
Example
We start with two classes in a belongs_to : has_many relationship, where
the Hiker class has a name attribute.
class ThruHike < ApplicationRecord
belongs_to :hiker, inverse_of: :thru_hikes
delegate :name, to: :hiker, prefix: false
end
class Hiker < ApplicationRecord
has_many :thru_hikes, inverse_of: :hiker
endAs in the case above, this works well. We can call thru_hike.name to
retrieve the delegated name value from the associated Hiker. But if the
hiker’s name rarely changes, we could instead cache it on the ThruHike model
to avoid having to touch the Hiker model each time we want to access the
name.
This can now be easily accomplished with delegate_cached using virtually the
same syntax as delegate.
class ThruHike < ApplicationRecord
belongs_to :hiker, inverse_of: :thru_hikes
delegate_cached :name, to: :hiker, prefix: false
end
class Hiker < ApplicationRecord
has_many :thru_hikes, inverse_of: :hiker
endSince ThruHike is now cacheing the value, you would need to create and run a
migration adding a name column to the thru_hikes table.
What you now get by using delegate_cached is:
- When
thru_hike.nameis nil, it updates itself with the delegated value. Subsequent accesses will return the cached value. - When the
thru_hikeupdates the value of itshikerassociation, abefore_savecallback updates thethru_hike.nameto the new value. - When the associated
Hikerupdates its name, anafter_savecallback updates the value inThruHike
There’s still more work to be done on delegate_cached to make it backwards
compatible with Rails 4.X, and other features I’m interested in adding. I
welcome bug reports, feedback, suggested improvements, and any other comments
on the Github site below.