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.

Published by Avdi Grimm

6 Comments

  1. Maybe using new instead of build inside self.build_fuzzy method would do the trick (I have not checked this solution). But I think using scoped 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.

    Reply
  2. 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]

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *