Let’s say we want a helper method #build_fuzzy
everywhere we have a collection of socks.
Sock.build_fuzzy # => #<Sock type: "fuzzy" id: nil drawer_id: nil> sock_drawer.socks.build_fuzzy # => #<Sock type: "fuzzy" id: nil drawer_id: 123>
At first we might think to put it in an a collection extension module:
class Sock < ActiveRecord::Base module SockCollectionHelpers def build_fuzzy build(type: "fuzzy") end end # ... end
Now we have to remember to add it to every Sock
association:
class Drawer < ActiveRecord::Base has_many :socks, extend: Sock::SockCollectionHelpers # ... end
And we also need to include it in Sock
:
class Sock < ActiveRecord::Base # ... extend SockCollectionHelpers end
…but this doesn’t work:
sock_drawer.socks.build_fuzzy # => #<Sock type: "fuzzy" id: nil drawer_id: 123> Sock.build_fuzzy # => Raises no method exception for Sock.build()
And anyway, we want this to be available globally without explicitly extending associations.
Next, we try adding a class method to the Sock
class. Since ActiveRecord association proxies delegate missing methods to the assoication class, it seems like this should work.
class Sock < ActiveRecord::Base def self.build_fuzzy build(type: "fuzzy") end # ... end
This is a complete failure. While the association does forward the .build_fuzzy
call to Sock
, once in the call it is operating in the context of the class object, which as we saw before has no #build
method.
sock_drawer.socks.build_fuzzy # Raises no method exception for Sock.build() Sock.build_fuzzy # => Raises no method exception for Sock.build()
However, a slight change makes everything work as hoped:
class Sock < ActiveRecord::Base def self.build_fuzzy scoped.build(type: "fuzzy") end end
Now when we call #build_fuzzy
on the class it builds a fuzzy sock unassociated with any drawer, and when we call it on an association it builds a fuzzy sock with the appropriate drawer ID set:
Sock.build_fuzzy # => #<Sock type: "fuzzy" id: nil drawer_id: nil> sock_drawer.socks.build_fuzzy # => #<Sock type: "fuzzy" id: nil drawer_id: 123>
All this thanks to the #scoped
method, which is aware of the current scope.
Thank you to Dan Kubb for figuring this out for me.
UPDATE: Fixed def build_fuzzy
to be def self.build_fuzzy
in the last two examples.
Maybe using
new
instead ofbuild
insideself.build_fuzzy
method would do the trick (I have not checked this solution). But I think usingscoped
is fine as well.There are more inconsistencies between AR classes and relations (for example classes don’t have
.each
method). They are annoying, but fortunately easy to fix with.scoped
.#new would not pick up the drawer_id in the association case.
If I’m looking at the right place, #new and #build are the same on associations: https://github.com/rails/rails/blob/v3.1.3/activerecord/lib/active_record/associations/collection_proxy.rb#L66
Huh, interesting.
At any right, the code this was extracted from needed more than just #build; it also needed to call various other methods which require a relation.
But thanks for pointing that out!
(edited it out, accidental comment!)
Does anyone see any issues with doing the following in recent versions of AR?
[code lang=”ruby”]
def all
super.extending Sock::SockCollectionHelpers
end
[/code]