11/28/2016 by Chris Selmer
Introducing delegate_cached

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
end

This 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
end

As 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
end

Since 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.name is nil, it updates itself with the delegated value. Subsequent accesses will return the cached value.
  • When the thru_hike updates the value of its hiker association, a before_save callback updates the thru_hike.name to the new value.
  • When the associated Hiker updates its name, an after_save callback updates the value in ThruHike

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.

View Source Code and Contribute

delegate_cached on Github

Comments