Wednesday, September 12, 2007

Autoload ActiveResource Accessors & Association Classes

If you're trying to use ActiveResource in a Rails app as a plug-and-play replacement to ActiveRecord, it really doesn't work like that. One way you can make your life a lot easier is to build a metaprogramming/reflection process into ActiveResource based on the process inside ActiveRecord.

As it turns out, this is easy to implement. ActiveRecord loads its database's schema on class definition using a SHOW FIELDS command. If you have control over the application ActiveResource is hitting, you can add a schema URL pretty effortlessly. Then, on the actual ActiveResource app, you just put an alias_method_chain on inherited in ActiveResource::Base. Add in an autoload method; have that method do the work of an attr_accessor, except don't actually use attr_accessor, because you need to put the attribute in the @attributes hash for it to be any use.

In some ways, this is like using an WSDL file, and it might give you the creepy-crawlies for that reason. In fact, however, it's a lot more elegant. It's dynamically generated, and its syntax is very clean, so you have none of the maintainence and coupling issues WSDL gives you.

It's also much more economical than attaching attr_accessors all over the place with class << self every time you need to use a form handler, and better from a performance standpoint than Rails' default behavior. If you're using ActiveResource stock, unmodified, out the box, you get a method_missing approach to @attributes which ActiveRecord abandoned a few versions ago in favor of more performant code-generation techniques, i.e., explicitly defining and attaching new methods, rather than passing all the work to method_missing. In a sense, method_missing is a lot like the iPhone when it was first released: amazingly cool, but way too expensive. (Fortunately, method_missing was never saddled with an AT&T service contract.)

If you've been following along, or attempting to actually code this from my description, you've probably noticed the big flaw: it doesn't account for association classes. That's pretty easy to fix. Use has_many (and its big family) in your ActiveResource models, and add the has_many family of class methods to ActiveResource::Base. The best way to implement it is to have it do exactly the same thing your autoload code already does in generating its accessor methods. Essentially, the only difference between has_many :widgets and attr_accessor :widgets should be that has_many also updates @attributes. The smart thing is to factor that out, so you can have both the autoloading code and the has_many family hit a class method with a name like add_attribute.

As you might guess, I have some code to show here, but I can't show it at the moment, partly due to corporate politics (LAME) and partly due to not having actually finished it yet. I literally just thought of the has_many part as I was writing this. We've got a lot of this working at my current project, however, and we should have it running in production very very soon.

3 comments:

  1. What did you end up using as the schema format for this?

    While I like the elegance of all this, I'd be a bit concerned about having too much of a dependency on the server app for your client app to function properly. If you only need the server app to be up while Ares is GETting, POSTing, etc, then if something goes wrong in the communication you can always recover, save the resource locally and sync it up later. However if you communicate at class definition time and the server is down your client would either crash completely or be missing a bunch of methods that the views expect, which would not be recovered until the client is restarted.

    Of course if you control both the server and the client, as it sounds like you do, then it probably isn't much of a problem. I think situations where the developer controls both server and client is probably the main target audience for Ares anyway.

    ReplyDelete
  2. That seems to be the case, about the target audience, although I don't know for sure. Realistically, as far as my project goes, we have the kind of dependency on the server one way or another - all this code really does for us is group it all in one place.

    In terms of the schema format, this was actually whisked off my to-do list by my manager, who's really a programmer. He literally stole it from me while I was sleeping. I've bitched him out about it in an e-mail, and I'll find out tonight whether that gets me in trouble - he's in Beijing, hence the sleep/wakefulness disparity.

    Anyway, all that just serves as a caveat that I didn't design the schema format. Basically, the schema format is XML, like this:

    model.columns.each
    "column" column.data "/column"
    end

    with angle brackets taken out, because Blogger eats them for breakfast. In other words it's just the same code you see in scaffolding, but used to do model reflection. I was pissed off about the pilfering, but I have to admit, his solution's pretty clean. If I was doing it - or if in another situation I do it again - I'll experiment with just using the code from rake db:schema:dump - I think it's an ARec object called SchemaDumper - so that you only have to make the call once. I hadn't imagined the has_many stuff, that was a workaround required by my manager's solution, and I can see both upsides and downsides to it. ideally the has_many and similar methods should be added dynamically based on the schema reflection; in practice, in this project, we'll probably have to add them by hand.

    ReplyDelete
  3. Hi Giles,

    I am thinking about implementing this, I really like the idea of building associations into ActiveResource, I have a well defined set of models which I would like to emulate with ARes as best as I can in a client.

    Do you have any source code you can share about implementing has_many and maybe belongs to into AciveResource::Base?

    ReplyDelete

Note: Only a member of this blog may post a comment.