04.17.07

ActiveResource: Associations and Methods

by Ben Myles

ActiveResource (ARes) has been much talked about, and is already quite usable. There’s a few getting started articles out there, but not much with any real depth. In this article I’ll take one step further and talk about associations and methods. Recommended reading for this article is listed at the end of the post.

After installing the ARes plugin, the first thing you want to do is create a parent class for your resources to inherit from. The parent class looks something like this:

billing_resource.rb:

class BillingResource < ActiveResource::Base
self.site = "http://127.0.0.1:4000" 
end

Here, we’re simply creating a BillingResource that acts as a parent to our resources such as CreditCard, Invoice and Subscription. You set the location of the remote application in the self.site variable in this class, so that you don’t have to set it for every other resource. You can also add any other common methods here.

Now, let’s create a Subscription resource:

subscription.rb:

class Subscription < BillingResource
def free?
amount_in_cents.to_i == 0
end
def credit_card
@credit_card ||= CreditCard.find(credit_card_id) if credit_card_id
end
def invoices
@invoices ||= connection.get("/subscriptions/#{id}/invoices.xml").collect { |h| Invoice.find(h['id']) } rescue []
end
def generate_and_pay_next_invoice
resp = connection.post("/subscriptions/#{id}.xml;generate_and_pay_next_invoice")
!resp.to_hash['location'].nil?
end
end

First off, you will notice our Subscription resource inherits from the parent BillingResource. You’ll also notice that we’re defining methods for the associations, such as credit_card and invoices. ActiveResource doesn’t automatically do this for you. It’s up to you to implement it.

ActiveResource provides a basic find() method for your resources. You can do Invoice.find(id) but you can’t do Invoice.find_all_by_subscription_id(x). If you try, you just get a NoMethodError:

>> Invoice.find_by_subscription_id(1)
NoMethodError: undefined method `find_by_subscription_id' for Invoice:Class

This isn’t a big deal. We can use the connection object to contact the remote resource at the correct RES Tful URL to retrieve the data we need. The relevant line from the code snippet above is:

@invoices ||= connection.get("/subscriptions/#{id}/invoices.xml").collect { |h| Invoice.find(h['id']) } rescue []

What’s happening here? We’re requesting, from the remote application, the XML version of the index action on the invoices controller for the subscription ‘id’. The XML version knows to just do a select => :id on the invoices, so the return payload is just a bunch of invoice id’s. In this example, I’m then iterating through each invoice id and loading the full invoice; you could do something more efficient if you need to. This all gets cached in the @invoices variable so that repeat hits to subscription.invoices don’t need to fetch the data again.

Your’re probably wondering what the invoices index action in the remote application looks like. It’s very simple:

def index
@subscription = Subscription.find params[:subscription_id]
format.xml do
render :xml => @subscription.invoices.find(:all, :select => 'id').to_xml
end
end

The credit_card method is a lot simpler than the invoices method, because it’s just a belongs_to relationship. We’ve already got the credit_card_id available, so we can just do a simple CreditCard.find(credit_card_id). Again, we’re caching it in the @credit_card variable to minimize repeat trips over the network.

Finally, you’ll notice the generate_and_pay_next_invoice method. This is actually a method on the Subscription model in the remote application, but because ARes doesn’t let you call methods remotely, we need to define it again. We won’t, however, repeat the logic of the method. Instead, we’ll do a connection.post to the generate_and_pay_next_invoice action in the remote subscriptions controller. The action in the remote application is pretty simple:

def generate_and_pay_next_invoice
@subscription = Subscription.find(params[:id])
@invoice = @subscription.generate_and_pay_next_invoice
respond_to do |format|
if @invoice
format.xml { head :created, :location => subscription_invoice_url(@subscription, @invoice) }
else
format.xml { head :ok }
end
end
end

If the remote action is successful, it will return the location of the new invoice resource. We can use that to make the method return true or false depending on its success:

!resp.to_hash['location'].nil?

I’m returning head :ok in this method instead of a 422 or 500 error status. That’s because it’s not supposed to raise any exceptions if there’s nothing for generate_and_pay_next_invoice to do. You could, however, easily return an error status:

format.xml { render :text => "Failed", :status => 422 }

In summary:

  • ARes does not let you call remote methods directly, you need to implement them.
  • ARes does not provide methods for associations, you need to implement them.

If you keep these points in mind, working with ARes becomes a lot more straight-forward.

There may be better ways to implement some of these items. The above has worked well for me, but if you know of better practices please leave a comment and I’ll keep the post alive by integrating the new information.

Recommended Reading

Comments

Dan Kubb about 20 hours later

I’ve always thought that if Rails would implement the OPTIONS method, we could use the body of the response (which has no predefined purpose) to display the nested and associated routes for a resource.

ActiveResource could execute OPTIONS and get a list of which methods are supported by the resource (via the Allow header) AND know what other routes are nested within the resource, or otherwise associated with it.

Leave a Comment