Implementing ActiveRecord-like associations for an API wrapper
I recently wrote ParseResource, which is a Ruby API wrapper for Parse.com's REST api.
Here's a some basic usage:
class Post < ParseResource
fields :title, :author, :body
end
p = Post.create(:title => "Hello world", :author => "Alan", :body => "ipso lorem")
The project is fairly young, and a feature I really want to implement is associations. Something like this:
class Author < ParseResource
has_many :posts
fields :name, :email
end
class Post < ParseResource
belongs_to :author
fields :title, :body
end
a = Author.create(:name => "Alan", :email => "alan@example.com")
p = Post.create(:title => "Associated!", :body => "ipso lorem", :author => a)
p.author.class #=> Author
p.author.name #=> "Alan"
a.posts #=> an array of Post objects
I'd love any advice, pointers, and pitfalls from anyone who has implemen开发者_运维技巧ted something similar as well as from anyone who has a grasp of Parse's REST API.
I've found using DataMapper ( http://datamapper.org ) it's pretty easy to get it working with almost any datastore. You can write an adapter that talks to your datastore and then use all of the power of DataMapper directly as if your data was in SQL. Here's a link that explains a bit about writing one of these adapters. http://www.killswitchcollective.com/articles/55_datamapperabstractadapter_101
It looks like Parse works by storing objects as a hash of key-value pairs. So basically you have an id and then a hash with which you can let your imagination run.
To do associations like ActiveRecord you need a primary key (e.g., Author.id) and a foreign key (e.g., Post.author_id). The Author.id is simple - just make it the id of the Parse object. Then store the author id for a post inside the post, keyed by 'author_id'. So that's the data side.
In the code there are several levels of implementation to consider. For retrieval you're aiming to make methods like this:
class Author
def posts
@posts ||= Post.find(:all, :id => id)
end
end
class Post
def author
@author ||= Author.find(author_id)
end
end
That's not too hard and can be done in many ways, for instance using metaprogramming. Harder is the save. What you're aiming for, at least from the Author side, is something like this:
class Author
def after_save
super
posts.each do |p|
p.author_id = id
p.save
end
end
end
Or rather I should say that's what you might be aiming for depending on the scenario. One of the pitfalls in implementing associations is deciding when to do stuff. You don't want to complicate your life, but you also don't want to go crazy with API calls. Consider simply updating the name of an author:
a = Author.find(1)
a.name = "Joe"
a.save
As written after_save
will load existing posts (it goes through posts
which sets @posts), set author_id on each post (which need not be done in this case), and then save the posts even though nothing has changed them. Moreover what if a post fails during save? In that case transactions are needed so you can rollback the whole thing and prevent an inconsistent state.
You can see in the ActiveRecord code there is a ton of logic surrounding the issue of how to handle children when a parent is saved. The result is the slick and transparent associations but all kinds of other things get involved; proxies, association classes, etc.
My advice is this. Decide if you really need slick and transparent associations. If not then metaprogram a few accessors and convenience methods and leave it at that. Otherwise, spend time studying the ActiveRecord association code directly or consider DataMapper which, AFAIK, gives you an ActiveRecord-like interface, including associations, with the ability to change data stores.
精彩评论