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:
- Encapsulates logic that is dedicated to the association itself
- Provides an expressive access to associated model objects
- 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.