「Ambitionのすべて」の編集履歴(バックアップ)一覧に戻る

Ambitionのすべて - (2008/04/25 (金) 00:34:31) のソース

This idea has been kicked around a whole lot, I know. In IRC. Dark alleyways. Sweaty dance halls. Courts of law. It’s this: instead of writing SQL, write Ruby. Generate the SQL behind the curtain.

Something like this:

User.detect { |u| u.name == 'Jericho' && u.age == 22 }
Or this: 
User.select { |u| [1, 2, 3, 4].include? u.id }
Or even this: 
User.select { |u| u.name =~ 'rick' }.sort_by(&:age)
It’d be cool, right?

Don’t get me wrong, we’re already able to express our queries in Ruby. There’s ez-where and Squirrel, and probably a ton more. But those are DSLs for querying with Ruby, not as Ruby. I want to use the straight-up Enumerable I know and love, nothin’ else. Call me old fashioned.

== An Ambitious Undertaking ==
Erlang’s Mnesia database is something like what I want: you write your queries in plain Erlang and they are translated into Mnesia-queries by walking the parse tree. Nice trick, but listen up: Ruby has a parse tree, too, and we can get at it pretty easily thanks to ParseTree.

So, we do. Introducing Ambition.

$ sudo gem install ambition -y
Play with it in your Rails console using the ActiveRecord logging hack:

$ script/console
>> ActiveRecord::Base.logger = Logger.new(STDOUT)
=> #<Logger:0x2814134 ...>
>> require 'ambition'
=> []
Some examples using an ActiveRecord model, followed by the SQL executed in the background:

User.first
"SELECT * FROM users LIMIT 1" 

User.select { |m| m.name != 'macgyver' }
"SELECT * FROM users WHERE users.`name` <> 'macgyver'" 

User.select { |u| u.email =~ /chris/ }.first
"SELECT * FROM users WHERE (users.`email` REGEXP 'chris') LIMIT 1" 

User.select { |u| u.karma > 20 }.sort_by(&:karma).first(5)
"SELECT * FROM users WHERE (users.`karma` > 20) 
 ORDER BY users.karma LIMIT 5" 

User.select { |u| u.email =~ 'ch%' }.size
"SELECT count(*) AS count_all FROM users 
 WHERE (users.`email` LIKE 'ch%')" 

User.sort_by { |u| [ u.email, -u.created_at ] }
"SELECT * FROM users ORDER BY users.email, users.created_at DESC" 

User.detect { |u| u.email =~ 'chris%' && u.profile.blog == 'Err' }
"SELECT users.`id` AS t0_r0 ... FROM users 
 LEFT OUTER JOIN profiles ON profiles.user_id = users.id 
 WHERE ((users.`email` LIKE 'chris%' AND profiles.blog = 'Err')) 
 LIMIT 1" 
And so forth. A big list of examples can be found in the README.

== Kicking Around Data ==
A good thing to keep in mind is that queries aren’t actually run until the data they represent is requested. Usually this is done with what I call a kicker method. You can call them that, too.

Kicker methods are guys like detect, each, each_with_index, map, and first (with no argument). Methods like select, sort_by, and first (with an argument) are not kicker methods and return a Query object without running any SQL.

As such, you can garner some information from a Query object:

>> user = User.select { |u| u.name == 'Dio' }
=> (Query object: call #to_sql or #to_hash to inspect...)
>> user.to_sql
=> "SELECT * FROM users WHERE users.`name` = 'Dio'" 
>> user.to_hash
=> {:conditions=>"users.`name` = 'Dio'"}
>> user.first  # => SQL is run
=> #<User:0x36896e4 ...>
Note the to_hash—Ambition doesn’t actually run any SQL, it just hands this hash to ActiveRecord::Base#find.

Anyway, kickers have useful implications for Rails apps. Take this controller:

class BandsController < ApplicationController
  def index
    @bands = Band.sort_by(&:name)
  end
end
Since no kicker method is called, @bands is just a Query object—no SQL run. The SQL is only run once we call each in our view:

<h1>Rocktastic Bands<h1>
<ul>
<% @bands.each do |band| %>
  <li><%= band %></li>
<% end %>
</ul>
Now, let’s say you grow a bit and want to a) fragment cache and b) reduce queries. Standard stuff.

Two birds, one stone:

<h1>Rocktastic Bands<h1>
<% cache do %>
  <ul>
  <% @bands.each do |band| %>
    <li><%= band %></li>
  <% end %>
  </ul>
<% end %>
If Rails finds a cached fragment, the SQL is never run. Slick.

== The Catch ==
This is pretty new, so watch the sharp edges. While we aren’t good at executing arbitrary Ruby inside the block, we can handle variables.

Practically speaking, instead of writing: 
User.select { |u| u.created_at = 2.days.ago }.first
Write: 
date = 2.days.ago
User.select { |u| u.created_at = date }.first
Instance variables and simple method calls work fine, too. Expect full Ruby support in a future release. (Expect it sooner if someone sends in a patch!)

== Big Dreams ==
Ideally, this thing could turn into something like Rack for databases. Query DataMapper, Sequel, or ActiveRecord using Ruby’s plain jane Enumerable API. Hey, maybe we can thrown an OODB or two into the mix?

The usual suspects:

Report feature requests, shortcomings, & bugs at Lighthouse. 
SVN’s at svn://errtheblog.com/svn/projects/ambition 
Code at http://projects.require.errtheblog.com/browser/ambition 
RDoc at Rock 

I’ve been running this on Cheat for a few days and it’s going real swell. Check the source for awesomeness like @sheets = Sheet.sort_by(&:title).

Watch this space as we grow up our little ambition.

Update: Oh yeah, KirinDave came up with the name Ambition. Thanks.

Update 2: Okay, added some stuff tonight: You can now do cross-table sort_bys: 
User.sort_by { |u| [ u.profile.name, u.ideas.karma ] }
That didn’t work before. Doh. I also added query support for any?, all?, and empty?—they do a COUNT behind the scenes, so feel free to give them crazy conditions. All three are kickers.

Also added were the entries and to_a kickers. User.to_a is the same same as User.find(:all). Works as an all-purpose kicker, too—User.select { |u| u.name == ‘kicker’ }.to_a and whatnot.

Finally, slice is now an alias for []. You guys can thank PJ for that one.

The new gem is out and it’s hot. I added some empty specs for destructive and constructive methods—dangerous thinking, I know. We’ll see.
記事メニュー
目安箱バナー