2007年12月24日月曜日

コードのインデントがおかしくなる…

なんでだ…
前から思ってたけど、Bloggerってコードのせるのにあんまり適したBlogじゃないなぁ…。
記事書いてるより、フォーマットなおしてる時間の方が長いような…。

Action Pack: Record identification

InfoQの記事 Ruby on Rails 2.0が公式リリースをもとに、Rails2.0の新機能を順に確認しつつ、知らない機能があれば詳しく調べてしまおうというもくろみの3つ目、Action PackのRecord identification。

InfoQの説明だけ見てると、なんのこっちゃという感じだが、サンプルを見てみるとなんとなくイメージはついてくる。
# person is a Person object, which by convention will
# be mapped to person_url for lookup
redirect_to(person)
link_to(person.name, person)
form_for(person)
URL生成系の各種メソッドが、引数としてモデルオブジェクトを取ることができるようになった、ということらしい。正直、あんまり使ったことないから便利さはよくわからないんだけど、新機能らしい。…さすがにこれだけで終わるわけにはいかないので、それぞれのメソッドを簡単に調べてみる。まずはredirect_toから。

・gems\actionpack-2.0.2\lib\action_controller\base.rb
module ActionController #:nodoc:
class Base
class << self
def redirect_to(options = {}, response_status = {}) #:doc:

if options.is_a?(Hash) && options[:status]
status = options.delete(:status)
elsif response_status[:status]
status = response_status[:status]
else
status = 302
end

case options
when %r{^\w+://.*}
raise DoubleRenderError if performed?
logger.info("Redirected to #{options}") if logger && logger.info?
response.redirect(options, interpret_status(status))
response.redirected_to = options
@performed_redirect = true

when String
redirect_to(request.protocol + request.host_with_port
+ options, :status=>status)

when :back
request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"],
:status=>status) : raise(RedirectBackError)

when Hash
redirect_to(url_for(options), :status=>status)
response.redirected_to = options

else
redirect_to(url_for(options), :status=>status)
end
end
モデルオブジェクトが渡された際に実行されるのは、一番下のelse~のところだろうか。ここだけじゃよくわからないので、さらにurl_forを追ってみる。
module ActionController #:nodoc:
class Base
class << self
def url_for(options = nil) #:doc:
case options || {}
when String
options
when Hash
@url.rewrite(rewrite_options(options))
else
polymorphic_url(options)
end
end
モデルオブジェクトが渡されて来た場合には、polymorphic_urlが呼び出されるようだ。
・gems\actionpack-2.0.2\lib\action_controller\polymorphic_routes.rb
module ActionController
module PolymorphicRoutes
def polymorphic_url(record_or_hash_or_array, options = {})
record = extract_record(record_or_hash_or_array)

namespace = extract_namespace(record_or_hash_or_array)

args = case record_or_hash_or_array
when Hash; [ record_or_hash_or_array ]
when Array; record_or_hash_or_array.dup
else [ record_or_hash_or_array ]
end

inflection =
case
when options[:action] == "new"
args.pop
:singular
when record.respond_to?(:new_record?) && record.new_record?
args.pop
:plural
else
:singular
end

named_route = build_named_route_call(record_or_hash_or_array,
namespace, inflection, options)
send!(named_route, *args)
end
モデルの情報を色々展開して、最後はbuild_named_route_callを呼び出し。
def build_named_route_call(records, namespace, inflection, options = {})
records = Array.new([extract_record(records)]) unless records.is_a?(Array)
base_segment = "#{RecordIdentifier.send!("#{inflection}_class_name", records.pop)}_"

method_root = records.reverse.inject(base_segment) do |string, name|
segment = "#{RecordIdentifier.send!("singular_class_name", name)}_"
segment << string
end

action_prefix(options) + namespace + method_root + routing_type(options)
end
結局、RecordIdentifierを使ってurl生成のための情報をひっぱってきて、組み立てて返している。やっとタイトルにつながった。

・gems\actionpack-2.0.2\lib\action_controller\record_identifier.rb
module ActionController
module RecordIdentifier
extend self
def plural_class_name(record_or_class)
singular_class_name(record_or_class).pluralize
end

def singular_class_name(record_or_class)
class_from_record_or_class(record_or_class).name.underscore.tr('/', '_')
end
このルートで使われているのが確認できるのはこれくらいだろうか。
link_toもform_forも、入り口が違うだけで最終的には同じルートを通っていた。細かい点でわからないところはいくつかあるが、機能の確認としてはここまでで終わっておく。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Multiview

Action Pack: Multiview

InfoQの記事 Ruby on Rails 2.0が公式リリースをもとに、Rails2.0の新機能を順に確認しつつ、知らない機能があれば詳しく調べてしまおうというもくろみの2つ目、Action PackのMultiview。

これは、Action Pack: Resources で:format について触れた際に少しだけ出てきた、HTML以外の多様なフォーマット(XMLとか、テキストとか…)をレンダリングするためのメカニズムについての記事らしい。
以前は、テンプレートのファイル名が「アクション名.rhtml」だったのが、複数ビューのサポートにあたって汎用的なメカニズムに置き換えられ、「アクション名.フォーマット名.レンダラ名」になったと書いてある。これにより、script/generateなどで生成されるテンプレートファイル名は、「アクション名.html.erb」となっている。実際にRails2.0でブックマークアプリで生成したアプリの「views\items」を確認してみると、「edit.html.erb」など、この形式のテンプレートが存在することを確認できる。
HTML以外のでの応用例としてはAtomのレンダリングが挙げられており、「アクション名.atom.builder」と書くことで、Builderというレンダラでレンダリングできるらしい。このあたりの機能を使うための実際のコードはscaffoldで生成されたコントローラ(controllers\item_controller.rb)にも入っていて、

・${app_root}\controllers\item_controller.rb
  def index
@items = Item.find(:all)

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @items }
end
end
こんな感じになっている。respond_toメソッドがどうなっているのか確認すると…

・gems\actionpack-2.0.2\lib\action_controller\mime_responds.rb
module InstanceMethods

def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block
block ||= lambda { |responder| types.each { |type| responder.send(type) } }
responder = Responder.new(self)
block.call(responder)
responder.respond
end
となっており、ResponderをActionControllerのサブクラスのインスタンスを渡しつつインスタンス化し、生成したResponderをブロック引数に渡してブロックをコールバックしている。Responderはどうなってるのかというと、

・gems\actionpack-2.0.2\lib\action_controller\mime_responds.rb
  class Responder #:nodoc:
def initialize(controller)
@controller = controller
@request = controller.request
@response = controller.response

@mime_type_priority = Array(Mime::Type.lookup_by_extension
(@request.parameters[:format]) || @request.accepts)

@order = []
@responses = {}
end

mime_type_priorityに、リクエストパラメータ内の:formatに対応するMime::Typeを追加している。後で出てくるが、この配列に追加したMime::Typeが優先して処理される。一方、コールバックされるブロックでは、

・${app_root}\controllers\item_controller.rb
class ItemsController < xml =""> @items }
end
end
こんな感じでformat(生成されたResponder)に対して「html」とか「xml」とかが呼び出されているわけだが、Responderにはこれら個別のフォーマットに対応したメソッドは用意されておらず、すべてmethod_missingで処理されるようになっている。上記の例ではブロックは指定されていないが、ブロックを渡すこともできるらしい。
class Responder #:nodoc:

def method_missing(symbol, &block)
mime_constant = symbol.to_s.upcase

if Mime::SET.include?(Mime.const_get(mime_constant))
custom(Mime.const_get(mime_constant), &block)
else
super
end
end
method_missingではブロックごと customメソッドに渡して…
class Responder #:nodoc:

def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type :
Mime::Type.lookup(mime_type.to_s)

@order <
< mime_type

@responses[mime_type] = Proc.new do
@response.template.template_format = mime_type.to_sym
@response.content_type = mime_type.to_s
block_given? ? block.call :
@controller.send(:render,
:action => @controller.action_name)
end
end
レスポンスレンダリング用の設定をした上で、ProcオブジェクトをMime::Typeに対応づけ、ハッシュに保存する。Procの内容はというと、ブロックが渡されていればブロックを呼び出し、いなければアクションを指定しつつコントローラのrenderメソッドを呼び出す、というもの。最後にrespond_toから以下のメソッドが呼び出され、この中で先ほど保存したProcが呼び出される。
class Responder #:nodoc:

def respond
for priority in @mime_type_priority
if priority == Mime::ALL
@responses[@order.first].call
return
else
if @responses[priority]
@responses[priority].call
return # mime type match found, be happy and return
end
end
end

if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
else
@controller.send :head, :not_acceptable
end
end
end
という感じで、ざっくり挙動がわかったところでAction PackのMultiviewについてはひとまず終了。renderあたりの細かい挙動は、別途とりあげます。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Record identification

2007年12月23日日曜日

Action Pack: Resources

まず最初は、Action Pack: Resourcesというもの。中身を読んでみたけど、そもそも拡張前の仕様がわからないからさっぱり意味がわからない。
というわけで、まずはサンプルとしてあげられている「routes.rb」について調べることにする。ちなみにroutes.rbは、configディレクトリの中に置いてあります。またまたGoogle検索してみると…

優しいRailsの育て方 routes

こんな素晴らしいページが。Migrationの時と同じBlogだ、感謝。
んで、これを読んでみると、routesはURLとコントローラ/アクションのマッピングを行うためのものらしい。基本的には、マッチするURLパターンと、マッチした場合に追加するリクエストパラメータ(キーと値)を指定できる。で、ここで
map.connect '', :controller => "berryz", :action=> "index"
みたいに指定すると、berryzという名前のコントローラのindexメソッドが呼び出される。どうやら、Railsはリクエストパラメータのcontroller値とaction値をもとに呼び出し先を決めているようだ。デフォルトでは「http://ホスト名:ポート番号/コントローラー名/アクション名/ID」というようなURLの、それぞれの値が使われるわけだが、これも特別扱いされているわけではなく、URL内の値がリクエストパラメータに自動的にマッピングするというroutesの機能を使って実現されているにすぎないようだ。
この、URL内の値をリクエストパラメータに自動的にマッピングする機能を使ったデフォルト動作の指定は、
map.connect ':controller/:action/:id'
この1行で行われている。マッチするURLのパターン内に、「:」を先頭につけた値が指定されていると、そのパス値はすべてのパターンにマッチする。マッチしたパス値は、定義された(「:」が先頭についている)値をキーとするリクエストパラメータの値として設定される。
上記の定義では、「http://ホスト名:ポート番号/ココ/...」が「:controller」にマッチし、その結果、リクエストパラメータとして「:controller => ココの値」が設定される。以下、「:action」や「:id」についても同じ処理が実行される。
これでようやく、URLで指定された値がなぜ勝手にIDとして設定されるのかがわかった。

試しに、自分のroutes.rbを見てみよう。bookmarkアプリを一通り作っただけの状態だと、routes.rbは以下のような状態になっている。

・${app_root}\config\routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :items

# The priority is based upon order of creation: first created -> highest priority.

# Sample of regular route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action

# Sample of named route:
# map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
# This route can be invoked with purchase_url(:id => product.id)

# Sample resource route (maps HTTP verbs to controller actions automatically):
# map.resources :products

# Sample resource route with options:
# map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }

# Sample resource route with sub-resources:
# map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller

# Sample resource route within a namespace:
# map.namespace :admin do |admin|
# # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
# admin.resources :products
# end

# You can have the root of your site routed with map.root -- just remember to delete public/index.html.
# map.root :controller => "welcome"

# See how all your routes lay out with "rake routes"

# Install the default routes as the lowest priority.
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
end
さきほど説明した、デフォルトルートは一番下に定義されている。
# Install the default routes as the lowest priority.
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
先ほどの話どおりの、コントローラー名/アクション名/IDを設定するもの以外に、もう1つ末尾に「:format」が追加されているものがある。URLの最後に、「.xxx」とついていれば、それが「:format」というパラメータで渡されるようだ。この値の使い道は、2.0で生成されたscaffoldコントローラを見てみれば大体わかる。
# GET /items
# GET /items.xml
def index
@items = Item.find(:all)

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @items }
end
end
この、respond_toの中で「:format」が参照され、値が存在した場合にはこの値に対応するMimeに関連した処理が実行される。例えば、「XXX.xml」というリクエストを送信すると、xmlに対応したMimeに関連する処理が実行された結果、XMLフォーマットのレスポンスが返されることになる。
以降、routes.rbの中のコメントアウトされた定義で、わかるものだけ確認してみる。

通常の定義
# Sample of regular route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action
この定義の例では、「http://ホスト名:ポート番号/products/xxx」というURLのリクエストは、catalogコントローラのviewメソッドにマッピングされる。先ほどの説明のとおり。

名前付き定義
# Sample of named route:
# map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
# This route can be invoked with purchase_url(:id => product.id)
これはルートに名前をつけるサンプルで、「map.connect」のかわりに「map.xxxx」と指定する。他は通常の定義と同じ。このように定義することで、Rubyコード中でこの名前を使って呼び出せるようになる。

さて、ここまでが基本知識。さきほどのコメントの中で、map.resources...と書かれたものの意味がよくわからない。またまたGoogleで調べてみると、とても素晴らしいBlogを見つけた。

Active Resouceの基本

これは、RESTfulなAPIとRailsのコントローラ/アクションを結びつけるための仕組みのようだ。例えば、routes.rb内の一番上に定義してある、
map.resources :items
この定義なら、
  • GET /items -> ItemsController#show
  • POST /items -> ItemsController#create
  • PUT /items -> ItemsController#update
  • DELETE items/ -> ItemsController#destroy
というようにマッピングされるらしい。なるほど。:formatとあわせれば、Ajaxで使いやすそうなサービスが簡単に実現できそう。routes.rbの例だと、
 # Sample resource route (maps HTTP verbs to controller actions automatically):
# map.resources :products
これに対応する。オプションを追加することもできるらしい。
 # Sample resource route with options:
# map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
これは、通常のmap.connectionと同様に静的にオプションを追加できる機能だろうか。サンプルはよくわからないけどとりあえず先に進む。
 # Sample resource route with sub-resources:
# map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
サブリソースを定義できるらしい。こいつもイマイチよくわからない。
 # Sample resource route within a namespace:
# map.namespace :admin do |admin|
# # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
# admin.resources :products
# end
これは、名前空間のサポートの例のようだ。コメントに書いてあるとおり、一段階パスをはさみ、そのパスをモジュールに対応づける機能らしい。一部わからないところはあるけど、ざっくりと概念はわかったのでとりあえず先に進むことにする。そもそも本題は、Rails2.0の新機能なんだし。で、これをふまえて新機能を読んでみると…

最初の話は名前空間のサポートの話で、ちょうど今出てきた機能。rake routesでルートの一覧が表示できるようになったということも書かれている。最後は、
  # /avatars/45 => AvatarsController#show
map.resources :avatars

# /people/5/avatar => AvatarsController#show
map.resources :people, :has_one => :avatar
こんな形で複数のリソースを同じコントローラにマッピングできるという話。これを見ると、先ほど出てきたサブリソースの定義というのは、エイリアスのような機能を指しているか、またはそういう形でも使えるようだ。
Active Resouce自体は、外部のRESTfulなサービスへの接続にも使える(というよりそれがメイン?)ライブラリなんだけど、InfoQの新機能紹介ではそこまで出てきていないので、今のところは保留にしておくことにする。別途RESTfulなサービスへの接続を試す機会があれば、深く掘り下げてみたい。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Multiview
Action Pack: Record identification

Rails2.0の変更点

InfoQに、Rails2.0の主な変更点について書いてあった。

Ruby on Rails 2.0が公式リリース


そもそも1.2も勉強中だったのでおこがましいが、まずは2.0で何が変わったのかを把握しておくことの必要性は、scaffoldでbookmarkアプリを作る際にイヤと言うほど味わった。
ということで、InfoQの新機能紹介記事の内容を1つずつ吟味していく。新機能といってもベースとなる機能があるものも多いので、その際には1.2のベースとなる機能も調べていく。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0でブックマークアプリ
Action Pack: Resources
Action Pack: Multiview
Action Pack: Record identification

2007年12月22日土曜日

Rails2.0でブックマークアプリ

Rails2.0はインストール済み、「rails bookmark」でbookmarkアプリの雛形までは作成されているという前提で話をすすめていく。
接続先のデータベース(MySQL)にもテーブルは一つも存在しない。この状態で、itemに対応するModelを生成する。
C:\Projects\OtherProjects\Rails\bookmark>jruby script\generate scaffold item url
:string title:string
create app/models/
create app/controllers/
create app/helpers/
create app/views/items
create app/views/layouts/
exists test/functional/
exists test/unit/
create app/views/items/index.html.erb
create app/views/items/show.html.erb
create app/views/items/new.html.erb
create app/views/items/edit.html.erb
create app/views/layouts/items.html.erb
identical public/stylesheets/scaffold.css
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/item.rb
identical test/unit/item_test.rb
skip test/fixtures/items.yml
create db/migrate
create db/migrate/001_create_items.rb
create app/controllers/items_controller.rb
identical test/functional/items_controller_test.rb
create app/helpers/items_helper.rb
route map.resources :items
ModelとControllerは無事に生成されている。migrateファイルも生成されているようだ。

テーブルの作成
次に、Migrateを使ってDBにテーブルを追加する。
C:\Projects\OtherProjects\Rails\bookmark\db\migrate>rake db:migrate
(in C:/Projects/OtherProjects/Rails/bookmark)
== 1 CreateItems: migrating ===================================================
-- create_table(:items)
-> 0.0460s
== 1 CreateItems: migrated (0.0460s) ==========================================
DBを確認してみると…
sqlite> .tables
items schema_info
sqlite> .schema items
CREATE TABLE items ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "url" varch
ar(255) DEFAULT NULL, "title" varchar(255) DEFAULT NULL, "created_at" datetime D
EFAULT NULL, "updated_at" datetime DEFAULT NULL);
という感じで、無事テーブルが出来た。サーバーを起動して、アプリケーションが動作しているかどうかを確認する。
C:\Projects\OtherProjects\Rails\bookmark>jruby script\server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2007-12-22 21:35:02] INFO WEBrick 1.3.1
[2007-12-22 21:35:02] INFO ruby 1.8.5 (2006-12-25) [i386-mswin32]
[2007-12-22 21:35:02] INFO WEBrick::HTTPServer#start: pid=172 port=3000
無事に動作していることを確認。ここで、Itemにdescriptionを追加してみる。DBに列を追加するためには、script\generate migration を使う。
C:\Projects\OtherProjects\Rails\bookmark>ruby script\generate migration AddDescr
iptionToItem description:text
exists db/migrate
create db/migrate/002_add_description_to_item.rb
これで、Migrationファイルは作成完了。ちなみに、やってみるまではわからなかったが、MigrationファイルはMigrationファイル名ごとに1つではなく、アプリケーション全体で1つのバージョンを持つ。ここで生成されたのはバージョン002でMigration名がadd_description_to_itemだが、バージョン001のMigration名はcreate_itemsになっている。ちなみに、現段階ではMigrationファイルが作成されただけで、DBスキーマにはまだ変更は反映されていない。DBスキーマに変更を反映するためには、rake db:migrationを使う。
C:\Projects\OtherProjects\Rails\bookmark>rake db:migrate
(in C:/Projects/OtherProjects/Rails/bookmark)
== 2 AddDescriptionToItem: migrating ==========================================
-- add_column(:items, :description, :text)
-> 0.1100s
== 2 AddDescriptionToItem: migrated (0.1100s) =================================
SQLiteを見てみると…
sqlite> .schema items
CREATE TABLE items ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "url" varch
ar(255) DEFAULT NULL, "title" varchar(255) DEFAULT NULL, "created_at" datetime D
EFAULT NULL, "updated_at" datetime DEFAULT NULL, "description" text);
このように、末尾にdescriptionが追加されていることがわかる。

画面への反映
以上でDBへの列定義追加は完了したが、画面(html.erb)にはDescription表示用の項目が反映されていないため、アプリケーションを起動してもDescriptionは出てこない。ViewにDescription用のコードを追加するには、手動でコードを追加するか、generate scaffoldで生成しなおすかくらいだろうか?
現段階ではscaffoldで生成しなおす方が手っ取り早いので、生成しなおしてみる。ちなみに、普通に生成すると、同じ名前のMigrationファイルが既に存在するために失敗する。
C:\Projects\OtherProjects\Rails\bookmark>ruby script/generate scaffold item url:
string title:string description:text
exists app/models/
exists app/controllers/
exists app/helpers/
exists app/views/items
exists app/views/layouts/
exists test/functional/
exists test/unit/
identical app/views/items/index.html.erb
identical app/views/items/show.html.erb
identical app/views/items/new.html.erb
identical app/views/items/edit.html.erb
identical app/views/layouts/items.html.erb
identical public/stylesheets/scaffold.css
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
identical app/models/item.rb
identical test/unit/item_test.rb
skip test/fixtures/items.yml
exists db/migrate
Another migration is already named create_items: db/migrate/001_create_items.rb
これを回避するためには、--skip-migrationオプションをつけて実行する。まぁ、失敗しててもViewは出力されているから別にいい気がするけど。
C:\Projects\OtherProjects\Rails\bookmark>ruby script/generate scaffold item url:
string title:string description:text --skip-migration
exists app/models/
exists app/controllers/
exists app/helpers/
exists app/views/items
exists app/views/layouts/
exists test/functional/
exists test/unit/
identical app/views/items/index.html.erb
identical app/views/items/show.html.erb
identical app/views/items/new.html.erb
identical app/views/items/edit.html.erb
identical app/views/layouts/items.html.erb
identical public/stylesheets/scaffold.css
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
identical app/models/item.rb
identical test/unit/item_test.rb
skip test/fixtures/items.yml
identical app/controllers/items_controller.rb
identical test/functional/items_controller_test.rb
identical app/helpers/items_helper.rb
route map.resources :items
script\serverでサーバーを起動してみると、無事にDescriptionが追加されていることが確認できる。

というわけで、ブックマークアプリはひとまず完成。この後は、1.2で途中まで進めていた内部構造の解析をすすめていく。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0の変更点
Action Pack: Resources
Action Pack: Multiview
Action Pack: Record identification

Migration

このあたりの記事が非常に参考になりました。

優しいRailsの育て方:Migration
MigrationのRDoc

要するに、
  • DBスキーマをRubyで記述することで物理DBの詳細に煩わされることなくアプリとDBを管理するメカニズム
  • Rubyで書かれたスキーマファイル をバージョン管理(それぞれのスキーマファイルには、バージョンを進める/戻すための操作を定義)することもできる
こういうことらしい。で、1.2の時代には手動で作成していたが、2.0からはgenerate scaffold時に(scaffoldに与えたプロパティ定義から)自動的に生成されるようになったみたいだ。
1.2の時代はgenerate scaffoldはDBスキーマの情報を動的に取得して各種ソースを生成していたが、これは、DBを先に修正した上でgenerate scaffoldするという、DB中心の開発を意味する。
2.0のようなscaffoldの扱いでは、Modelを修正してそこから各種ソース+Migrationファイルを生成し、生成されたMigrationファイルを(rakeで)実行することでDBをアップデートするという、Model中心の開発になると思われる。これは、元々Railsから派生したフレームワークである、Groovyの思想に近くなっている。

ということで、次は最初からbookmarkアプリを作成し、Modelに新たなプロパティを追加した後にMigrationからDBに反映させるという手順を試してみることにする。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Multiview
Action Pack: Record identification

Rails2.0のscaffoldは前とだいぶ違うらしい

Rail2.0のscaffoldを使ってみた

ここに非常に良い記事が。なんかRakeのえらい人かな?これを読んでみると、2.0からはscaffold時に自動的にMigrationファイルが生成されるようになったらしく、(Migrationファイルの生成に必要なためか)generate scaffold時にはモデルに含まれるデータ型を明示的に指定しないといけなくなったらしい。つまり、1.2までのように指定なしで生成しようとするとうまくいかないということだ。実際にやってみると、上記の記事に書いてあるように、途中で止まってしまってControllerが作成されなかった。

というか、Migrationって何?なんかDBに関連しているみたいだけど、1.2時代にすらまったくMigrationを使ってないのでまったくわからない。2.0のMigrationの説明はほとんどないので、まずは1.2のMigrationについて勉強してみる。概念としてはほぼ同じみたいだし。

関連リンク:
Rails2.0リリース
Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Multiview
Action Pack: Record identification

Rails2.0リリース

というわけで、もたもたしている間にRails2.0(.2)が出てしまった。せっかくなので、これから先はRails2.0を勉強していきたい。
相変わらずJRubyを使う予定だけど、JRubyの方も1.0.3がリリースされているので、こちらも同じくアップデートしておく。ちなみに、古いRailsは1.0.3では動作しなくなったみたい。

JRuby: 1.0.3 addresses compatibility issues, 1.1 performance update

インストール方法はJRuby1.0.2+Rails1.2の時と変わらず、「gem install rails」でOK。
以降、JRuby で Ruby on Railsと同じ手順で進めながらbookmarkアプリを作ってみる。
C:\Projects\OtherProjects\Rails>jruby %RUBY_HOME%\bin\rails bookmar
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
create config/environments
create config/initializers
create db
create doc
create lib
create lib/tasks
create log
create public/images
create public/javascripts
create public/stylesheets
create script/performance
create script/process
create test/fixtures
create test/functional
create test/integration
create test/mocks/development
create test/mocks/test
create test/unit
create vendor
create vendor/plugins
create tmp/sessions
create tmp/sockets
create tmp/cache
create tmp/pids
create Rakefile
create README
create app/controllers/application.rb
create app/helpers/application_helper.rb
create test/test_helper.rb
create config/database.yml
create config/routes.rb
create public/.htaccess
create config/initializers/inflections.rb
create config/initializers/mime_types.rb
create config/boot.rb
create config/environment.rb
create config/environments/production.rb
create config/environments/development.rb
create config/environments/test.rb
create script/about
create script/console
create script/destroy
create script/generate
create script/performance/benchmarker
create script/performance/profiler
create script/performance/request
create script/process/reaper
create script/process/spawner
create script/process/inspector
create script/runner
create script/server
create script/plugin
create public/dispatch.rb
create public/dispatch.cgi
create public/dispatch.fcgi
create public/404.html
create public/422.html
create public/500.html
create public/index.html
create public/favicon.ico
create public/robots.txt
create public/images/rails.png
create public/javascripts/prototype.js
create public/javascripts/effects.js
create public/javascripts/dragdrop.js
create public/javascripts/controls.js
create public/javascripts/application.js
create doc/README_FOR_APP
create log/server.log
create log/production.log
create log/development.log
create log/test.log
ぱっと見あんまり変わったところはなさそうで、とりあえず普通に生成はできている。ここからは、DBにテーブルを作らないといけないわけだけど…Rails2.0.2からデフォルトDBがSQLiteに変更されたらしい。せっかくだから使ってみたいけど、SQLiteの使い方なんて全然わからないぞ。ていうか、SQLite自体インストールされてないのでは…?というわけで、色々調べてみた。

SQLiteのインストールと動作確認
適当に検索してみたところ、SQLiteは別途インストールが必要で、Windows用のSQLiteはdllとして提供されている模様。さらに、sqlite.exeなるコマンドラインツールからDB操作できるようだ。

[Rails] RailsでSQLite♪
SQLite + ActiveRecordを試す
Imitation with Ruby on Rails

ということで、SQLiteの公式サイトから「sqlitedll-3_5_4.zip」と「sqlite-3_5_4.zip」をダウンロードして、展開。まずはデータベースエンジンである(はずの)sqlite3.dllを「C:\WINDOWS\system32」にコピーする。これで、少なくともオンメモリのDBとしては動作するようになったはず。
ただし、これだけではデータベースの操作ができない。データベースを操作するためには、sqlite3.exeを使う。とりあえずは、試しにsqlite3.exeを実行してみる。
SQLite version 3.5.4
Enter ".help" for instructions
sqlite> .database
seq name file

--- --------------- ----------------------------------------------------------

0 main

sqlite>
「.database」でデータベースの一覧が表示できる…らしいが、作ってないので当然1つも存在しない。が、動作はしている模様。

bookmarkアプリ用データベース&テーブル作成
動作確認が(たぶん)できたところで、bookmark用のデータベースを作成する。bookmarkアプリのDBの定義は、config\database.ymlにあるので、確認しておく。
# SQLite version 3.x
# gem install sqlite3-ruby (not necessary on OS X Leopard)
development:
adapter: sqlite3
database: db/development.sqlite3
timeout: 5000
この設定だと、db\development.sqlite3を作ればOKな模様。以下のコマンドで、DBファイルを指定してsqlite3.exeを起動する。
C:\Projects\OtherProjects\Rails\bookmark\db>sqlite3 development.sqlite3
SQLite version 3.5.4
Enter ".help" for instructions
sqlite> create table items(
...> id integer primary key,
...> url varchar(255),
...> title varchar(255),
...> description text
...> );
sqlite> insert into items(url,title,description)values('http://www.google.co.jp'
,'Google','Mos popular web search engine');
sqlite> select * from items;
1|http://www.google.co.jp|Google|Mos popular web search engine
sqlite> .exit

という感じで、bookmarkテーブルの作成完了を確認。あとは、MySQLの時と同じようにやれば動作するはずだ。

モデル/コントローラの作成
まずはモデルを作る。

C:\Projects\OtherProjects\Rails>cd bookmark

C:\Projects\OtherProjects\Rails\bookmark>jruby script\generate model item
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/item.rb
create test/unit/item_test.rb
create test/fixtures/items.yml
create db/migrate
create db/migrate/001_create_items.rb


続いてコントローラ。

C:\Projects\OtherProjects\Rails\bookmark>jruby script\generate controller item
exists app/controllers/
exists app/helpers/
create app/views/item
exists test/functional/
create app/controllers/item_controller.rb
create test/functional/item_controller_test.rb
create app/helpers/item_helper.rb
ItemControllerに「scaffold :item」を追加して、起動。
C:\Projects\OtherProjects\Rails\bookmark>jruby script\server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2007-12-22 16:37:22] INFO WEBrick 1.3.1
[2007-12-22 16:37:22] INFO ruby 1.8.5 (2007-12-15) [java]
[2007-12-22 16:37:23] INFO WEBrick::HTTPServer#start: pid=1102920 port=3000

http://localhost:3000/item

にアクセスしてみると…

undefined method `scaffold' for ItemController:Class

なんと、エラー・・・。ActionControllerにscaffoldメソッドがなくなったのか??と思ったが、よく見るとこれはItemControllerのindexメソッド(?)で起きているらしい。scaffoldがなくても勝手に動くようになってる…?試しにscaffoldを削除すると、今度は違うエラーが。

MissingSourceFile in ItemController#index

no such file to load -- sqlite3
どうやら、database.ymlのsqlite3用アダプタがないから起きているらしい。sqlite3用アダプタをGemでインストールすれば解決か?
C:\Projects\OtherProjects\Rails\bookmark>gem install sqlite3-ruby
Select which gem to install for your platform (java)
1. sqlite3-ruby 1.2.1 (mswin32)
2. sqlite3-ruby 1.2.1 (ruby)
3. sqlite3-ruby 1.2.0 (mswin32)
4. sqlite3-ruby 1.2.0 (ruby)
5. sqlite3-ruby 1.1.0 (mswin32)
6. sqlite3-ruby 1.1.0 (ruby)
7. sqlite3-ruby 1.0.1 (mswin32)
8. sqlite3-ruby 1.0.1 (ruby)
9. Skip this gem
10. Cancel installation
> 1
Successfully installed sqlite3-ruby-1.2.1-mswin32
Installing ri documentation for sqlite3-ruby-1.2.1-mswin32...
Installing RDoc documentation for sqlite3-ruby-1.2.1-mswin32...
で、もう一度ItemControllerにアクセスしてみると…

SyntaxError in ItemController#index

C:/ProgramFiles/Java/JRuby/jruby-1.0.3/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.1-mswin32/lib/sqlite3_api.so:0: Invalid char `\220' in expression
どうも、動かないらしい。実行環境をRubyに変更すると動くようになったため、JRubyの問題のようだが、いまいちよくわからない。原因究明については、別途エントリでやる予定。

以前までのエントリで、RubyプログラムやRailsの開発方法についてならJRubyとRubyでほぼ違いがないことがわかった。Rails2.0でデフォルトDBがSQLite3になったことに加え、AirやGoogle Gearsでの採用などSQLiteへの注目が高まっている。なので、多少趣旨を変更することになるかもしれないが、ここから先はJRuby+MySQLではなくRuby+SQLite3で進めていきたい。

追記:
このエントリでのRailsの動作のさせ方は、2.0での正式なやり方ではありません。詳しくは、以下の関連リンクのRails2.0のscaffoldは前とだいぶ違うらしいとかRails2.0でブックマークアプリとか見てください。

関連リンク
Rails2.0のscaffoldは前とだいぶ違うらしい

Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Multiview
Action Pack: Record identification