Extending has_many association proxy

The association types has_many and its companion has_and_belongs_to_many in Ruby on Rails are both extendible. It is a convenient approach to extend a Proxy object for various reasons:

  1. Encapsulates logic that is dedicated to the association itself
  2. Provides an expressive access to associated model objects
  3. Is combinable with collection proxy object methods

The example

In the example, a User has a name and a birthday:

rails g model User name:string birthday:date
rails g model Event title:string user:belongs_to

Furthermore, several events can be associated:

class Event < ApplicationRecord
  scope :beginning_after, ->(time) { where('starts_at > ?', time) }
end

class User < ApplicationRecord
  has_many :events

  def next_birthday
    current_birthday = birthday.change(year: Date.current.year)
    return current_birthday if current_birthday.future?
    current_birthday + 1.year
  end
end

The method User#next_birthday calculates the next users birthday.
The following query finds all user events, which begin after that next birthday:

user.events.beginning_after(user.next_birthday)
# => [#<Event:0x000000040f17a8 id: 2, ... ]

However, it looks somewhat awkward to pass the next birthday date explicitly, especially since the events association is directly attached to the user.
A little cumbersome.

Association extension

Any has_many and has_and_belongs_to_many Association can be expanded with a block and accessing the proxy_association object within it. This also includes the owner, that means the object itself (in this case the user):

class User < ApplicationRecord
  has_many :events do
    def after_next_birthday
      beginning_after(proxy_association.owner.next_birthday)
    end
  end

  def next_birthday
    current_birthday = birthday.change(year: Date.current.year)
    return current_birthday if current_birthday.future?
    current_birthday + 1.year
  end
end

The call looks way more elegant and Rails like:

user.events.after_next_birthday
# => [#<Event:0x000000040f17a8 id: 2, ... ]

Combined association extensions

Of course, all proxy methods can be combined.
In the example, another proxy method [] (term) was introduced:

class User < ApplicationRecord
  has_many :events do
    def after_next_birthday
      beginning_after(proxy_association.owner.next_birthday)
    end

    def [](term)
      events_table_name = proxy_association.reflection.klass.table_name
      where("LOWER(#{events_table_name}.title) LIKE ?", "%#{term.to_s.downcase}%")
    end
  end

  def next_birthday
    current_birthday = birthday.change(year: Date.current.year)
    return current_birthday if current_birthday.future?
    current_birthday + 1.year
  end
end

The method allows to filter events by their title (a LIKE search). Sure, this query selection part would be transferred to a named scope.
However, at this point, it was implemented directly in the proxy method to show that proxy_association can provide even more information about the association (reflection). The association_proxy.target can be useful, too.
A combined query:

user.events[:family].after_next_birthday

generates the following SQL:

SELECT events.*
FROM events
WHERE events.user_id = 1 
  AND (LOWER(events.title) LIKE '%family%')
  AND (starts_at > '2017-03-17')

In the case, additional proxy methods are useful, it is easy to implement elegant association extensions.