Die Assoziationstypen has_many und sein Kollege has_and_belongs_to_many in Ruby on Rails sind jeweils beide erweiterbar. Aus verschiedensten Gründen ist es komfortabel, das Proxy Objekt zu erweitern:
- Kapselt Logik, die direkt zur Assoziation gehört
- Ermöglicht einen ausdrucksstarken Zugriff auf assoziierte Objekte
- Ist mit anderen Collection Proxy Objektmethoden kombinierbar
Das Beispiel
In dem Beispiel hat ein User neben dem Namen auch ein Geburtsdatum:
rails g model User name:string birthday:date
rails g model Event title:string user:belongs_to
Zusätzlich können mehrere Events assoziiert sein:
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
Die Methode User#next_birthday berechnet den nächsten Geburtstag.
Folgende Query findet alle Events des Users, die nach dessen nächstem Geburtstag beginnen:
user.events.beginning_after(user.next_birthday)
# => [#<Event:0x000000040f17a8 id: 2, ... ]
Es wirkt allerdings etwas unbeholfen, das Datum das nächste Geburtstagsdatum explizit übergeben zu müssen, zumal die events Assoziation schon direkt an dem User hängt.
Etwas umständlich.
Erweiterung der Assoziation
Jede has_many und has_and_belongs_to_many Assoziation kann mit einem Block erweitert und innerhalb dessen auf das proxy_association Objekt zugreifen. Das beinhaltet den owner, also das Objekt selber (in diesem Fall der 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
Der Zugriff sieht nun sehr viel eleganter und Rails like aus:
user.events.after_next_birthday
# => [#<Event:0x000000040f17a8 id: 2, ... ]
Association Extensions kombinieren
Natürlich lassen sich sämtliche Proxy Methoden miteinander kombinieren.
In dem Beispiel wurde eine weitere Proxy Methode hinzugefügt:
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
Die Methode ermöglicht eine Filterung der Events nach Titeln (LIKE-Suche). Sicherlich würde man auch diese Selektion in einen Named Scope überführen.
An dieser Stelle wurde sie aber direkt in der Proxy Methode implementiert, um zu zeigen, dass proxy_association noch andere Informationen über die Assoziation liefern kann (reflection). Weiterhin sinnvoll kann association_proxy.target sein.
Eine kombinierte Anfrage:
user.events[:family].after_next_birthday
generiert folgendes SQL:
SELECT events.*
FROM events
WHERE events.user_id = 1
AND (LOWER(events.title) LIKE '%family%')
AND (starts_at > '2017-03-17')
Im Falle, dass weitere Proxy Methoden sinnvoll sind, können damit sehr elegante Assoziationserweiterungen implementiert werden.