It's been a little while since I've last posted in this series. During that time, we released Rails 3.0 beta, and announced the launch of RailsPlugins.org. Plugin authors have registered almost 150 plugins, with 40 already boasting compatibility with Rails 3. Over the next few weeks, we're going to roll out some more features to help users find projects to help get updated, so keep an eye out.
Today I'm going to talk about the features we added to Rails 3 to provide ORM agnosticism. When we first started, the idea of such agnosticism was pretty fuzzy. As we approached the beta, we became convinced that we would shoot for the moon and give DataMapper, Sequel and other ORMs first-class access to all of the same parts of the framework that ActiveRecord had. Again, this sounds pretty fuzzy, so let me lay it out for you.
ActiveRecord is a Framework Extension
You read that right. While Rails itself ships with ActiveRecord as a default, the Railties gem, responsible for bootstrapping Rails, knows nothing about ActiveRecord. In order to achieve this, we had to make that bootstrapping process much more pluggable. While this is amazing for extensions, like DataMapper, that want to replace a built-in framework, it also exposes more functionality to any Rails extension distributed as a gem.
In short, Railties now coordinates the initialization process, which involves the activation of a number of individual Railtie subclasses. Cute, no? Every Rails framework has its own Railtie subclass, which provides a number of useful pieces of functionality:
- The ability to add a key (like
config.active_record) to an Application's configuration, and assign it defaults that will exist before Application configuration. For instance, a plugin could use this functionality to set a configuration key as an Array, so the Application could simply do
config.action_view.view_paths << "my_path". While this isn't exactly earth-shattering, it levels the playing field between Rails components, like ActiveRecord, and third-party extensions, like DataMapper or Haml
- The ability to create additional generators that hook into Rails' default generators. For instance, Rails' scaffold generator invokes sub-generators for stylesheets, template engine, test framework, helpers, and ORM. For instance, when a user installs Haml as a gem, it can register itself as the handler for template engine generators, and provide replacements for each case where Rails provides a default implementation. In this case, Haml would replace the template engine generators provide by ActionController's Railtie
- The ability to supply Rake tasks to load when the user invokes the Rake command. If DataMapper supplies the same named Rake tasks as those supplied by the ActiveRecord Railtie, the user can completely remove ActiveRecord and replace it with DataMapper, retaining all of the named Rake tasks. This would allow other tasks, such as those used in testing, to automatically prepare the database and other tasks it performs by invoking tasks with certain names (like
- Supply a log subscriber to integrate seamlessly with the uniform request logging. This allows extensions like DataMapper to add their timings into the log output. In this case, DataMapper simply replaces the "Model" timing that ActiveRecord provides, allowing a level of very tight integration with the rest of the framework
- Specify initializers that should run at specific points in the initialization process. This allows extensions to set things up very early in the boot process, but then defer some setup until after the user has configured their application, or after specific parts of the initialization process have occurred. Each initializer also receives an instance of the Application object, giving it full access to the user's configuration
In fact, if you take a look at ActiveRecord's deep integration with the rest of the framework (which is roughly equivalent to its highly coupled implementation in Rails 2.3), you'll find that it looks extremely similar to DataMapper's equally deep integration. In fact, a major goal of the work we put into improving modularity in Rails 3 was to maintain the same level of stack integration we've historically had, while exposing the same toolchain for those who wanted to replace only certain elements of the stack.
The ActiveModel API
Giving DataMapper access to the same level of integration with Railties as ActiveRecord is one of two major pillars in making Rails truly ORM agnostic. The second part is decoupling Rails' historic connection between ActiveRecord, its ORM, and ActionPack, its controller and view layer. In this area, Rails takes a holistic, very conventional approach to linking the model and the view using REST principles.
In particular, domain objects, persisted via the ORM, can have canonical URLs that are used pervasively throughout the controller and view layer. The specific URLs can be configured via the router, and Rails 3's router is particularly powerful, but once configured, objects do have canonical URLs.
@post = Post.first redirect_to @post #=> Location: /posts/orm-agnosticism redirect_to Post #=> Location: /posts form_for @post #=> <form action="/posts/orm-agnosticism" method="PUT"> form_for Post.new #=> error_messages_for @post #=> a representation of the validation errors that exist on the object link_to @post.title, @post #=> <a href="/posts/orm-agnosticism"> # Rails and Merb Merge: ORM Agnosticism (Part 5 of 6) # </a> posts_path #=> "/posts" post_path(@post) #=> "/posts/orm-agnosticism" @posts = Post.where(:author => "wycats") render @post #=> renders "_post.html.erb", passing in @post as a local render @posts #=> renders a collection of "_post.html.erb", passing in each # Post instance as a local in turn respond_with @post #=> performs content negotiation between the formats # the client is willing to Accept (xml, json, html), # and the formats the @post object is willing to # provide. The available providable formats are # transparently determined by introspecting the @post # object. Additionally, if the object has not been persisted, # redirect it back to the editing page for further modification. # If it has been persisted, redirect it back to the index (or # some other appropriate path).</form>
In short, Rails 2.3 provided a fair bit of integrated functionality between ActiveRecord and ActionPack. For instance, to determine what an object's canonical URL was, Rails called an internal
.class.naming method on it, which provided the information needed to form the canonical URLs. And while other ORMs could attempt to emulate these internals, there was no guarantee that they'd be used in the same way from release to release (and, in fact, they evolved quite a bit, making them a moving target at best).
In Rails 3, we opened them up, exposing an explicit public API via ActiveModel. The ActiveModel API requires that objects respond to
to_model, returning an Object that complies with the larger ActiveModel API. This allows objects that already implement methods with the same names as those required by the API to create a facade that presents itself to ActionPack as a fully compliant model.
The API itself is fairly small, with a few methods around validation, the ability to determine whether an object has persisted or not (which can be safely stubbed by objects without persistence), and a number of methods that tell ActionPack how to convert the object into a canonical URL or template name. By doing this, Rails has completely decoupled ActionPack from ActiveRecord directly, and ActiveRecord becomes just one of many ORMs to implement the ActiveModel API.
The DataMapper ORM has already released a Rails extension (called dm-rails) that implements both the Railtie functionality and ActiveModel, making it a full drop-in replacement for ActiveRecord. The Sequel ORM is also hard at work on their Railtie and ActiveModel adapter.
The really great thing about all of this is that we didn't need to sacrifice one bit of the (some have said overly) tight integration between various parts of Rails, to provide the ability to swap in entirely different components with the same apparent "coupling" that the Rails frameworks have with each other.
As always, questions and comments are welcome!