Occasional encounters with Ruby on Rails.

Friday, January 25, 2008

ActiveResource as full members of your business logic?


ActiveResource, introduced with Rails 2.x, allows Rails developers to interact with remote resources in a manner similar to ActiveRecord objects essentially bringing them into play for the business layer of the local application. Or that's what you'd hope. Out of the box, ActiveResource simply gives the developer a window into the attributes of the remote resource at the time it was requested. Associations to other resources in the remote application are left behind. So are the business rules that the resource implements. If an ActiveResource object is going to be a full participant in your local application you will need some of those associations and methods that were left behind.



Not surprisingly, you can pass along those associated objects (has_one, has_many, etc) and the results of business logic with a little bit of work. It all starts with to_xml...



Serving it up with to_xml


In Rails 2.x (if not before), ActiveRecord::Base provides a handy to_xml method to help serialize your ARec as XML. Take a look at the default controller scaffolded by Rails 2.x and you'll see something like this:



  1. def show

  2.   @person = Person.find(params[:id])


  3.   respond_to do |format|

  4.     format.html # render show.html.erb

  5.     format.xml {render :xml => @person.to_xml}

  6. end



The @person.to_xml call will iterate through all the attributes read from the database build and build an xml envelope with them.



<?xml version="1.0" encoding="UTF-8"?>

<person>

<born-on type="date">1967-11-12</born-on>

<created-at type="datetime">2008-01-23T14:10:46-05:00</created-at>

<first-name>Andrew</first-name>

<id type="integer">1</id>

<last-name>Vanasse</last-name>

<middle-name nil="true"></middle-name>< /span>

< nickname>Andy</nickname>

< updated-at type="datetime">2008-01-23T14:10:46-05:00</updated-at>

</person>


The to_xml method also accepts some very helpful parameters.



:include

Just like ARec itself, this will cause the named associations to be included in the xml serialization.

:methods

The results of executing the specified instance method on the object will be included in the serialization.


We can use those two parameters to extend our resource into something more helpful to the 'local' application. For example, if Person has_many :addresses and has an instance method called #age, then @person.to_xml(:include=>:addresses, :methods=>:age) would return the following.



<person>

<born-on type="date">1967-11-12</born-on>

<created-at type="datetime">2008-01-23T14:10:46-05:00</created-at>

<first-name>Andrew</first-name>

<id type="integer">1</id>

<last-name>Vanasse</last-name>

<middle-name nil="true"></middle-name>

<nickname>Andy</nickname>

<updated-at type="datetime">2008-01-23T14:10:46-05:00</updated-at>

<age type="integer">41</age>

<addresses type="array">

<address type="HomeAddress">

<activated-on type="date" nil="true"></activated-on>

<addressable-id type="integer">1</addressable-id>

<addressable-type>Person</addressable-type>

<city>Mytown</city>

<created-at type="datetime">2008-01-24T15:10:23-05:00</created-at>

<deactivated-on type="date" nil="true"></deactivated-on>

<id type="integer">1</id>

<postal>23456</postal>

<state>SC</state>

<street>123 Someone Other Road</street>

<updated-at type="datetime">2008-01-24T15:10:23-05:00</updated-at>

</address>

</addresses>

</person>


And then some magic happens


Having rendered the object using to_xml and included the necessary associations and instance method call results does a number of things for the remote application. First, it helps solve a form of the infamous N+1 problem. The N+1 problem would be worse over the internet, of course, where all sorts of latency issues could crop up when accessing each of the N associated objects. Second, since neither the actual object nor a proxy is returned, returning the results of an instance method call gives the remote application some important information that it could not otherwise know without reproducing the logic.


There is something very important to keep in mind when rendering with the :methods option of to_xml: you should only render objects that reflect state. You are not returning a handle on the method only the results of executing the method so you should only expose calculated values in this way.


ActiveResource, of course, was developed with all this in mind. Since the default rendering of the XML includes a 'type' attribute, ActiveResource can reflect against the XML and instantiate a local object that looks like the remote object. Because of that, not only do the attributes appear the same on both ends but the instance method results rendered via the :methods parameter react the same way as well. Moreover, if ActiveResource objects exist on the local system to match the associated objects then the rendered associations will be re-hydrated on the local system using those ARes objects. In the example above, if the local system had an Address ActiveResource (subclassed as HomeAddress) then my_person.addresses would be an array of those Address ARes objects.



Putting it all together


ActiveResource.find(:first), find(id) and find([id1, id2, ...]) all perform a get on the singular resource path. If the remote system is a Rails solution this maps to the show action. Similarly, the find(:all) natively maps to the index action. To take advantage of the way ARec and ARes play together around to_xml, then, we should modify these two actions.




  1. class PersonController < ApplicationController

  2. find_options = {:include=>:addresses}

  3. xml_options = {:include=>:addresses, :methods=>:age}


  4. # GET /people.xml

  5. def index

  6. @people = Person.find(:all, find_options)


  7. respond_to do |format|

  8. format.html # index.html.erb

  9. format.xml { render :xml => @people.to_xml(xml_options) }

  10. end

  11. end


  12. # GET /people/1.xml

  13. def show

  14. @person = Person.find(params[:id])


  15. respond_to do |format|

  16. format.html # show.html.erb

  17. format.xml { render :xml => @person.to_xml(xml_options) }

  18. end

  19. end


  20. ...


  21. end

No comments: