out of time
-
Three changes I’d like to see to DataMapper before 1.0
DataMapper is an excellent Ruby ORM, brought to you by (roughly) the same rock stars who built Merb and are now working on building the first production-ready version of Rails. DataMapper boasts some obvious advantages over ActiveRecord - a small core with a gem-based plugin framework, an object store (to enforce one-object-per-row within a session), and a very clever strategy for implicitly avoiding n+1 query inefficiency. However, there are a few places where DM doesn’t shine - most of them would be easy enough to fix, but would involve changes in the API, so pre-1.0’s the time to do it. Here’s my wish list:
1. Get rid of
has 1/has nfor declaring associationsActiveRecord defines the class methods
has_oneandhas_manyfor declaring 1:1 and 1:n assocations respectively. This works fine. DataMapper, on the other hand, defineshas, which is written like this:has 1, :user has n, :usersThere are a couple of gripes I have here. First, the
hasmethod is doing completely different things depending on the first argument passed. When a method’s logic works like that, it’s usually a candidate to be two separate methods. It reminds me ofActiveRecord::Base#find, a method which does three different things, depending on the first argument passed. Mercifully, DataMapper defines a different method for each of those three things -#get,#first, and#all, respectively. They shouldn’t then go and make the same mistake with the::hasmethod.Second gripe: that
nthere is major code smell. As it turns out, thenclass method on DataMapper::Resource returns the Float value Infinity. That does make a sort of semantic sense, but declaring this class method just to provide some minor syntactic sugar for one declaration is a Ruby antipattern. This seems to me to be a case of someone getting a little too excited about how English-like Ruby can look.My last, minor, gripe is that
has_one :useris slightly more readable thanhas 1, :user. That comma ruins it for me a little.2. Don’t extend Symbol for non-general use
ActiveRecord’s ability to construct SQL queries using Ruby is woefully limited - if you want to test for any conditions other than equality or IS NULL, you’ve got to write SQL into your conditions (which, as far as I’m concerned, qualifies as string programming and is thus a Bad Thing). That’s clearly a weakness - most ORMs I’ve come across have provided a full criteria API allowing the construction of complex, robust queries in the ORM’s language. DataMapper doesn’t have that, but it does provide what at first seems to be an improvement on the situation in ActiveRecord. An example from DM’s API documentation:
Person.all(:age.gt => 30)Oh, hey, we can query for conditions other than equality without writing SQL! Here’s the problem: DataMapper defines the methods Symbol#gt and its friends in order to do this. Those methods on Symbol, a core class, have absolutely no meaning outside of that one use inside the DataMapper conditions, but they’re defined for every Symbol in the object space, regardless of its use, once you include dm-core. That’s stinky. Ruby is almost unparalleled in the ease it provides for writing beautiful DSLs - someone should take advantage of that for writing criteria APIs for Ruby’s main ORMs.
3. Take migrations seriously
One of the most exciting features brought to the table by Rails is migrations. While I’m sure the Rails team didn’t invent the idea of database migrations, Rails introduced me to the idea; I suspect that’s true for many people. Migrations act like version control for your database; they work more or less as a collection of diffs, just like a revision history in git. The fundamental idea behind migrations is that any given version of your codebase corresponds to a particular database schema; by keeping migrations in a set of files inside version control, you guarantee that any time a new revision of the codebase is checked out, the database can be updated to correspond to it. This is absolutely crucial for production applications that are backed by databases.
However, the DataMapper team seems to be ambivalent about the idea of migrations. DataMapper provides three mechanisms for making modifications to your database schema:
automigrate, which simply blows away your database tables and recreates them from the property information declared in your model;autoupgrade, which attempts to modify existing tables to match the property information declared in your model; and regular migrations, which work like their counterpart in ActiveRecord.automigrateis a nice toy for playing around in development, but is obviously useless in production applications, since you can’t just blow away your production data every time you want to modify your schema.autoupgradeis a nice idea but can’t possibly cover all of the possible changes you want to make.dm-migrations, a plugin indm-morethat provides equivalent functionality to ActiveRecord’s migrations, with a couple of nice enhancements - in particular, uniqueness based on name, rather than index/timestamp; and a nicer-looking DSL than what ActiveRecord does. The problem withdm-migrationsis that it’s so feature-poor it’s practically unusable. You can’t even set column lengths.In an ideal world, we’d have a nice standalone migrations tool for Ruby - after all, migrations are about schema definition in relational databases, and they’re conceptually distinct from your ORM. Indeed, DataMapper is flexible enough to work with schema-free databases; the idea of a schema isn’t even inherint in DM’s architecture. There’s really no reason that migrations should be bundled with it. In the short term, though,
dm-migrationsis there; it’s just not where it should be. Before 1.0, I hope the DataMapper team gives migrations the attention they deserve.I’m entirely convinced that before long, DataMapper will be the de facto Ruby persistence layer. It’s fast, well-engineered, and feature-rich. Here’s hoping the DM team is able to erase these few blemishes from what is otherwise an excellent library.