Rails勉強会@東京 第15回

18日の Rails勉強会 に行ってきた。

前半

前半は4つのセッションに分かれた。私はCapistranoセッションのオーナーになった。

先日会社でデプロイするときにミスをやらかしてしまって、デプロイツールの重要性が身に染みた。それで今まではそれほどちゃんと触ったことはなかったけれども、ひとつCapistranoを触ってみようと思ってセッションを開いた。

CapistranoRubyで書かれたデプロイツールで、Rakeをデプロイ作業向けに強化したようなものだ。基本はRakeなのでRakeとかMakeとかAntとかを使ったことがあれば怖くない。

デフォルトではRailsアプリケーションの構成に合わせた設定になっているけれども、設定(レシピと呼ばれる)を変更すれば任意のアプリケーションに適用可能である。実際、 はてな ではPerlアプリケーションに適用しているらしい。次の条件を満たす環境であれば何でもデプロイできる。

  • 手元の端末からデプロイ先サーバーにSSHログインできる

    • CapistranoSSH経由でデプロイ先サーバーを操作するため。
  • バージョン管理システムを使っている。

  • デプロイ先サーバーからバージョン管理サーバーにアクセスできる

    • Capistranoはバージョン管理サーバーからリソースを取ってきてデプロイするため。
    • レシピをカスタマイズすればアクセスできなくても大丈夫そうだけれども、大変だろう。
  • UNIXライクな環境である。

    • Capistranoがshellコマンドを利用するためだと思う。
    • Windowsにも対応できなくはないけれども大変らしい。

資料

" Agile Web Development with Rails "でも第2版のほうではCapistranoに簡単に触れている。その他には、Railsのサイトにある オンラインマニュアル と、それからソースコードが資料じゃないだろうか。

インストール

Rubygemsがあればインストールは簡単。

$ gem install --include-dependencies capistrano

これでPATHの通ったところにcapコマンドがインストールされる。依存物としてRubyからSSHを扱うライブラリなどが一緒に入るはず。

サンプルアプリケーションの作成

サンプルとして次のようなモデルをCRUDする簡単なアプリケーションをscaffoldで作成した。

class Teacher < ActiveRecord::Base
  has_many :lessons
  validates_presence_of :name
end

class Lesson < ActiveRecord::Base
  belongs_to :teacher
  validates_presence_of :title
end

で、これに対してまずはCapistranoを適用する。AWDwR第2版には「FastCGIはもう古い。Lighty以外ではモジュールもあんまりメンテされてないし。これからはPound×Mongrelですよ」と書いてあって、Mongrelの場合の例が書いてあるけれども、今回は気にせずFastCGIで行く。

$ cap --apply-to . Test
 exists config 
 create config/deploy.rb 
 exists lib/tasks 
 create lib/tasks/capistrano.rake 

これで、2つのファイルが作成された。capistrano.rakeのほうはRakeからcapコマンド相当の操作を行うためのタスク定義。deploy.rbのほうが、レシピを定義する雛形。以下はこのdeploy.rbをカスタマイズしていく。

レシピの構造

レシピの中身は大きく3つに分かれている。1つは変数定義。もう1つはロールの定義、最後がタスクの定義。

変数

変数定義には次の構文を使う。

set :application, "capistrano-test"
set :repository, "file:///......../svn/#{application}/"
set :lazy { gets }

このように変数に値を割りあてる。拡張リテラル記法で他の変数を参照することも可能な模様。ブロックを渡せば遅延評価できる。アプリケーションの名称applicationと、リポジトリの所在repositoryは必須の変数。これ以外にもいくつかcapistrano定義の変数があるけれども、詳しくは雛形にコメント化されて書いてあるのでそれを見れば分かる。

あと、ドキュメントにはないけれども、変数rakeを定義してみたらRakeのパスとして認識してくれた。

ロール

ロールの定義には次の構文を使う。

role :web, 'localhost'
role :app, 'localhost'
role :db, 'localhost', :primary => true 

ロールというのはそのまんまサーバーに割り振られている役割のこと。タスクを実行するに当たって、例えばデータベースのセットアップはDBサーバーにだけ実行する、コードの配置はアプリケーションサーバーにだけ実行する、という風に対象を限定たいことがあるけれども、そのためにロールを使う。Capistranoの拡張されたRake構文ではタスク定義の際に対象となるロールを指定できる。

最後のdbロールの場合のように、任意のオプションを付けてロールを更に細かく分類することもできる。

Capistranoのデフォルトのタスク群はweb, app, dbという3つのロールを使用している。各々の意味は見たまま。あと、migrationの適用みたいなタスクはオプション:primary => true付きのdbロールだけを対象としているので:primary => trueなdbタスクがないとrakeしたときに怒られる。

タスク

で、タスク定義。これはもうRakeタスクの定義構文そのまま。

desk "説明文"
task :task_name, :roles => [:web, :app] do
  # タスクの処理
end

ただし、実行対象となるロールを限定するオプションを取れるのと、それからいくつかの特徴的なヘルパーメソッドがある。たとえばrunヘルパーは引数として受け取った文字列をshellコマンドとしてリモートサーバーで実行する。

それ以外

capコマンドが生成してくれたレシピの雛形には以上の3つが書かれているけれども、Railsではおなじみのように、capistranoレシピの実態は単なるRubyコードなので、その気になれば任意の処理を実行できる。

使ってみる

そんなわけで、仮にDB, APP, Webの各サーバーを全部localhostに設定して動かしてみる。

$ cap setup
$ cap cold_deploy

"cap setup"はリモートサーバーにcapistrano定義のディレクトリ構造を生成する。"cap cold_deploy"は初回のデプロイに使用する。これでアプリケーションをリモートに配備してくれる。FastCGIプロセスの立ち上げや、mongrel clusterの立ち上げも設定に応じてやってくれる模様。

ここで、開発環境のほうのアプリケーションを弄る。データベーススキーマの変更も試したかったので、Teacher - Lessonの関連を1:nからn:nにして、Teachingモデルを介してhas_many :throughでつなぐようにした。

これで、

$ cap deploy_with_migration

すると、リモートに新しいバージョンをデプロイした後、rake migrateを走らせてくれる。

後半: rBatis

iBatisRuby版であるところのrBatisというのがあるらしいのでみんなで触ってみた。

iBatisもrBatisもどちらも、分厚いO/R Mapping APIを介するよりは直接SQLを書きたい人のためのMapperということになる。というわけでwkwk tktkしながらrBatisを見てみたけれども、Active Recordの薄いラッパーとして書かれていることが判明した。

RailsActiveRecordにはもともとfind_by_sqlというメソッドがあってこれを使えば結構何でもできる。ただ、この方法を使うとどうしてもソースコードの見た目は良くない。rBatisはActiveRecordを包んで、もう少し宣言的に見えるようにしてくれる。たとえば、Productをcategoryによって選択するようなメソッドを定義する場合。

class Product < ActiveRecord::Base
  def self.find_by_category(category)
    find_by_sql [<<-'SQL', category]
      SELECT * FROM products WHERE category = ?
    SQL
  end
end

これがfind_by_sqlを使った場合。これをrBatisで書くとこうなる。

class Product < RBatis::Base
  statement :select, :find_by_category do |category|
    [<<-'SQL', category]
      SELECT * FROM products WHERE category = ?
    SQL
  end
end

まぁ、find_by_category程度であれば何も書かなくてもARのdynamic finderで済んでしまうんだけれども、実際にはもっと複雑なクエリを行なうとしよう。

もろはしさんはrBatisの宣言的なやり方にしびれていたけれども、私はそこまでのメリットを感じない。確かに宣言的に書けるのは良いことだけれども、もともとfind_by_sqlもかなり宣言的に書けるわけで、要するに「もっと感じのよいAPIを定義しました」ということでしかない。

ARに対するrBatisはmigrationに対する migration2 と似たようなものか。だとすると、rBatisという名前はずいぶん大げさでないか、というのが私の感想。悪くはないけど名前に負けてるよね、と。Rails ARはActiveRecordとは言ってもPofEAAのActive Recordパターンよりは随分リッチな機能がたくさん付いているし、もともとfind_by_sql上等という設計思想だから結局iBatisとはAPIセットの違いしかないのかもしれない。

細かな比較は 勉強会のWiki にまとめがある。

SQLの外部化

私はそれよりも、舞波たんが実践していると言っていたSQLの外部化のほうに興味がある。上のようにヒアドキュメントでSQLをソースに埋め込むと、find_by_sqlの面目躍如たる複雑なクエリの時にソースが非常に読みづらくなる。そこで、SQLを外のファイルに追い出しましょう、と。

私もそのやり方のメリットはS2Daoで随分感じていたし、賛成。で、その場で適当に外部のSQLとメソッドを関連づけるプラグインを書いてみた。 その後、ちゃんと動くようにしたら結構大きくなったけど、それをhttp://svn.coderepos.org/share/lang/ruby/active_record_template/ から取得できる。

  • method missingを拾って、対応するSQLファイルが存在すればそれを元にメソッドを作成する。
  • SQLファイルにはERBが使える

    • メソッドの引数に渡したHashを元にERB評価コンテキストのローカル変数が設定される。
    • このへんのAPIはまだ一考の余地あり。Dynamic Finderみたいにメソッド名から取った方がいいのか。
    • Erb, quote を参考に、埋め込み変数はデフォルトでquoteされるようにした。quoteせずに文字列をそのままSQLに埋め込みたいときは、raw_sqlヘルパーを使う。<%=raw_sql "SELECT * FROM hoge"%>
    • S2Daoライクに、メソッド名に基づいてCRUDのいずれであるかを勝手に判別する。
    • 明示的に規約をオーバーライドするAPIを作るとRails的でよろしいかも。
    • メソッド名の規約はまだ一考の余地あり。一応S2DaoRubyのEnumerableのメソッドを参考にした。
  • 現在はクラスメソッドしか用意してない。create, update, deleteについてはあとでインスタンスメソッド版も作る。

    • クラスメソッド版との関係をどうしたものだろう。selfのattributesを展開して渡してやると整合性を取りやすいか。
    • 使ってみて、何が嬉しいのか考える。
    • ご意見募集。
  • 詳しくは テストケース 参照。