The short answer
Use class methods, and call them on your collection objects.
Given Customer has_many Orders, and Orders has_one Payments, add class methods to your Order model:
class Order < ActiveRecord::Base
has_one :payment
belongs_to :customer
def self.paid
find :all, :include => :payment, :conditions => ['payments.order_id']
end
# Returns the unpaid orders. See <tt>paid</tt> for more information.
def self.unpaid
find :all, :include => :payment, :conditions => ['payments.order_id is null']
end
end
You can call these class methods through your collection methods:
c = Customer.find(12)
c.orders # This is the collection method added by has_many :orders
c.orders.paid
c.orders.unpaid
The long answer
# Our simple example is for a Customer
# that has_many Orders, and each Order
# has_one Payment.
class Customer < ActiveRecord::Base
# See <tt>Customer::paid</tt> for how to get
# all of the paid and unpayed orders.
has_many :orders
end
class Order < ActiveRecord::Base
has_one :payment
belongs_to :customer
# Returns all the paid orders. A paid order
# is any order that has a corresponding Payment
# object.
#
# You can use this after a collection method. In
# this example, Customer has_many Orders, and Orders
# has_one Payment, so you can do:
#
# c = Customer.find(12)
# c.orders # This is the collection method added by has_many :orders
# c.orders.paid
# c.orders.unpaid
#
# The key thing to notice here is that this isn't a
# regular method. It's a method of the class itself,
# and Rails lets you call these class methods on the
# collection object.
#
# Here's another way to think about it: collection objects
# don't return a set of objects. This line:
#
# c.orders
#
# doesn't return an array of orders. What it returns is
# an "association proxy." The proxy knows how you've called
# it, and if you call it by itself, it'll just return
# an array of the objects you're looking for.
#
# If you call it with something else attached to it, like:
#
# c.order.paid
#
# what happens is a little different.
#
# <tt>c.order</tt> returns
# the association proxy, and then the association proxy is
# sent the :paid message. The association proxy doesn't have
# a method called :paid defined, so #method_missing is called.
# The association proxy does know what kind of objects it holds
# (Orders) so it asks the Order class if the Order class can
# respond to the :paid message. Order::paid does exist, since
# we defined it as a method on the Order class. The association
# proxy starts to build a query using #with_scope, so the
# class method that looks like it would just return every unpaid
# order only returns the unpaid orders that are in the right scope.
# In this case, the scope is Orders that have the right
# customer_id.
def self.paid
find :all, :include => :payment, :conditions => ['payments.order_id']
end
# Returns the unpaid orders. See <tt>paid</tt> for more information.
def self.unpaid
find :all, :include => :payment, :conditions => ['payments.order_id is null']
end
end
# In a real payment, you'd want to include more information.
# For this example, the fact that a payment exists means
# that the Order is paid.
class Payment < ActiveRecord::Base
belongs_to :order
end
The trick is that methods on the class itself can be used in finders.