はじめに

Ruby on Rails/サーバ起動時の処理を読むでは、WEBrickサーバが起動するまでを読みました。それでは、

http://localhost:3000/accountbook/

というhttpリクエストを受け取った場合にどのような処理が行われるかを見ていくことにしましょう。

DispatcherServlet($GEM_HOME/gems/lib/rails/webrick_server.rb)

WEBrickサーブレットはサーバからserviceメソッドを呼ばれます。serviceメソッドでは以下の処理が行われています。

  1. handle_fileメソッドを呼んで通常ファイルを処理する
  2. 通常ファイルとして処理できなかった場合はhandle_dispatchメソッドを呼ぶ

handle_dispatchメソッドの前半部分は以下のようになっています。

def handle_dispatch(req, res, origin = nil) #:nodoc:
  data = StringIO.new
  Dispatcher.dispatch(
    CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")), 
    ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, 
    data
  )

Dispatcher($GEM_HOME/gems/rails/lib/dispatcher.rb)

Dispatcher.dispatchメソッドです。条件分岐や例外処理を省略すると以下の処理が実行されます。

request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
prepare_application
controller = ActionController::Routing::Routes.recognize(request)
controller.process(request, response).out(output)
reset_after_dispatch

ActionController::CgiRequest, CgiResponseはそれぞれ、ActionController::AbstractRequest, AbstractResponseから派生しているようです。CgiRequest, CgiResponseは$GEM_HOME/gems/lib/actionpack/lib/action_controller/cgi_process.rb、AbstractRequestは同じディレクトリのrequest.rb、AbstractResponseは同じディレクトリのresponse.rbにそれぞれ定義されています。

Dispatcher.prepare_application

prepare_applicationメソッドです。

def prepare_application
  if Dependencies.load?
    ActionController::Routing::Routes.reload
    self.preparation_callbacks_run = false
  end

  prepare_breakpoint
  require_dependency 'application' unless Object.const_defined?(:ApplicationController)
  ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
  run_preparation_callbacks
end

Dependencies.load?がtrueの場合は毎回ルーティング処理をやり直しているようです。Dependencies.load?メソッドはスクリプト読み込みの方法がloadの場合trueです。アプリケーションディレクトリにあるconfig/environments/development.rbを見ると以下のようになっています。ちなみに、test.rbやproduction.rbはtrueになっているのでリクエストごとに再読込は行われません。

# In the development environment your application's code is reloaded on
# every request.  This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false

読み込み方法がいつ設定されるかというとRails::Initialize#processメソッドから呼ばれるinitialize_dependency_mechanismメソッドで設定されます。

def initialize_dependency_mechanism
  Dependencies.mechanism = configuration.cache_classes ? :require : :load
end

次にブレークポイントの設定を行っていますがとりあえず無視することにします。

次にapplication.rbが読み込まれています。ApplicationControllerはアプリケーションディレクトリのapp/controllers/application.rbに定義されているクラスです。require_dependencyメソッドは$GEM_HOME/gems/activesupport/lib/active_support/dependencies.rbに定義されており、ファイルを読み込むことで新しく定義された定数の検出などいろいろ興味深いのですが長くなるので止めます(説明するのがめんどくさいとも言う:-))

次のActiveRecord::Base.verify_active_connections!は、まだ接続がないはずなので何もしないはずです。

コールバックも登録してないはずなので無視しましょう。

ActionController::Routing::Routes.recognize ($GEM_HOME/gems/action_pack/lib/action_controller/routing.rb)

それではリクエストからコントローラを引き当てているらしいActionController::Routing::Routes.recognizeに移ります。ActionController::Routing::Routesは前回見たようにActionController::Routing::RouteSetオブジェクトです。

def recognize(request)
  params = recognize_path(request.path, extract_request_environment(request))
  request.path_parameters = params.with_indifferent_access
  "#{params[:controller].camelize}Controller".constantize
end

request.pathは要求されたパスでhttp://localhost:3000/accountbook/にアクセスしたとしたら"/accountbook/"となります。またextract_request_environmentメソッドはリクエスト情報からリクエストメソッドだけを取り出しているようです(プラグインとかのオーバーライドを想定しているようです)。

recongnize_pathメソッドです。前回設定したroutes(ActionController::Routing::Routeオブジェクトの配列)からリクエストパスに反応するものを検索しています。

def recognize_path(path, environment={})
  path = CGI.unescape(path)
  routes.each do |route|
    result = route.recognize(path, environment) and return result
  end
  raise RoutingError, "no route found to match #{path.inspect} with #{environment.inspect}"
end

ActionController::Routing::Route#rocognizeメソッドです。無限再帰?

def recognize(path, environment={})
  write_recognition
  recognize path, environment
end

ActionController::Routing::Route#write_recognition

write_recognitionメソッドに進みましょう。

def write_recognition
  # Create an if structure to extract the params from a match if it occurs.
  body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
  body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"

  # Build the method declaration and compile it
  method_decl = "def recognize(path, env={})\n#{body}\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  method_decl
end

どうやらrecognizeメソッドは動的に書かれているようです。動的に書かれるrecognizeメソッドは以下のようになります。

def recognize(path, env={})
  if (match = #{Regexp.new(recognition_pattern).inspect}.match(path))
    params = parameter_shell.dup
    #{recognition_extraction * "\n"}
    params 
  end
end

ActionController::Routing::Route#recognition_pattern

recognition_patternメソッドは以下のようになっています。

def recognition_pattern(wrap = true)
  pattern = ''
  segments.reverse_each do |segment|
    pattern = segment.build_pattern pattern
  end
  wrap ? ("\\A" + pattern + "\\Z") : pattern
end

前回を参照すると、segmentsには以下の順序でオブジェクトが格納されています。

  1. DividerSegment('/'), is_optional == true
  2. ControllerSegment(:controller), is_optional == false
  3. DividerSegment('/'), is_optional == true
  4. DynamicSegment(:action), is_optional == true
  5. DividerSegment('/'), is_optional == true
  6. DynamicSegment(:id), is_optional == true
  7. DividerSegment('/'), is_optional == true

DividerSegment(の親クラスのStaticSegment)のbuild_patternメソッドは以下のようになっています。

def build_pattern(pattern)
  escaped = Regexp.escape(value)
  if optional? && ! pattern.empty?
    "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
  elsif optional?
    Regexp.optionalize escaped
  else
    escaped + pattern
  end
end

DynamicSegmentのbuild_patternメソッドです。ControllerSegmentも同じものが使われます。

def build_pattern(pattern)
  chunk = regexp_chunk
  chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0
  pattern = "#{chunk}#{pattern}"
  optional? ? Regexp.optionalize(pattern) : pattern
end
def regexp_chunk
  regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)"
end

ControllerSegmentではregexp_chunkメソッドがオーバーライドされています。Routing.possible_controllersメソッドはアプリケーションディレクトリにあるapp/controllers内に「何とか_controller.rb」があるとしたら「何とか」が返ってくることになります。

def regexp_chunk
  possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
  "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
end

順を追って正規表現を構築してみましょう。

  1. (?:/)?
  2. (?:([^/;.,?]+)(?:/)?)?
  3. (?:(?:/)?\Z|/([^/;.,?]+)(?:/)?)
  4. (?:([^/;.,?]+)(?:(?:/)?\Z|/([^/;.,?]+)(?:/)?))?
  5. (?:(?:/)?\Z|/([^/;.,?]+)(?:(?:/)?\Z|/([^/;.,?]+)(?:/)?))
  6. (?i-:(accountbook))(?:(?:/)?\Z|/([^/;.,?]+)(?:(?:/)?\Z|/([^/;.,?]+)(?:/)?))
  7. (?:(?:/)?\Z|/(?i-:(accountbook))(?:(?:/)?\Z|/([^/;.,?]+)(?:(?:/)?\Z|/([^/;.,?]+)(?:/)?))

ん〜、見てると気分悪くなりますが、確認してみましょう。

http://localhost:3000/accountbook/
/(?i-:(accountbook))(?:(?:/)?\Z)にマッチ
http://localhost:3000/accountbook/show/1
/(?i-:(accountbook))(?:/([^/;.,?]+)(?:/([^/;.,?]+)))にマッチ

合っているようです。

ActionController::Routing::Route#recognition_extraction

次に、recognition_extractionメソッドです。

def recognition_extraction
  next_capture = 1
  extraction = segments.collect do |segment|
    x = segment.match_extraction next_capture
    next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
    x
  end
  extraction.compact
end

各segmentについてどうなるかを見ていきましょう。

  1. match_extractionの結果はnil, next_capture => 1
  2. match_extractionの結果は"params[:controller] = match[1].downcase if match[1]", next_capture => 2
  3. match_extractionの結果はnil, next_capture => 2
  4. match_extractionの結果は"params[:action] = match[2] if match[2]", next_capture => 3
  5. match_extractionの結果はnil, next_capture => 3
  6. match_extractionの結果は"params[:id] = match[3] if match[3]", next_capture => 4
  7. match_extractionの結果はnil, next_capture => 4

以上のことから動的に書かれたrecognizeメソッドは以下のようになります。

def recognize(path, env={})
  if (match = /\A(?:(?:/)?\Z|/((?i-:(accountbook)))(?:(?:/)?\Z|/([^/;.,?]+)(?:(?:/)?\Z|/([^/;.,?]+)(?:/)?))\Z/.match(path))
    params = parameter_shell.dup
    params[:controller] = match[1].downcase if match[1]
    params[:action] = match[2] if match[2]
    params[:id] = match[3] if match[3]
    params 
  end
end

魔法ですね:-)

Dependencies.load_missing_constant ($GEM_HOME/gems/activesupport/lib/activesupport/dependencies.rb)

結果、ActionController::Routing::Routes.recognizeメソッドの戻り値としてユーザ定義のAccountbookControllerが返されます。

・・・ん?AccountbookControllerっていつの間に読み込まれたのでしょうか?今まで見てきた中でaccountbook_controller.rbを読み込んでいるところは見かけなかったのですが。

dependencies.rbが怪しいと思ったのですがどう呼ばれているかさっぱりわからないので実際の読み込みを行っているload_fileメソッドに罠(?)を仕掛けてみました。

  def load_file(path, const_paths = loadable_constants_for_path(path))
p path
p caller

で引っかかったのが、

"script/../config/../app/controllers/accountbook_controller.rb"
[".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:94:in `require_or_load'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:250:in `load_missing_constant'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:454:in `const_missing'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:466:in `const_missing'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/inflector.rb:250:in `constantize'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/core_ext/string/inflections.rb:148:in `constantize'",
 ".../ruby-1.8.5-p12/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/routing.rb:1284:in `recognize'"

いつの間にかconst_missingなんてできたんですねぇ。ともかく、const_missingの仕組みとRubyのクラス名⇔ファイル名マッピング規則により、AccountbookControllerが要求されたときに自動的にaccountbook_controller.rbが読み込まれているようです。

ActionController::Base.scaffold ($GEM_HOME/gems/actionpack/lib/action_controller/scaffolding.rb)

一番単純なRailsアプリケーションは以下のようになっています。

app/controllers/application.rb

class ApplicationController < ActionController::Base
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_accountbook_session_id'
end

app/controllers/accountbook_controller.rb

class AccountbookController < ApplicationController
  scaffold :outgo
end

クラス定義中でscaffoldメソッドを使用するとhttp://localhost:3000/accountbook/にアクセスすることでoutgosテーブルの内容が表示されるようになります。ActionController::Baseに移る前にscaffoldメソッドについて見ていきましょう。

まず前半部分

def scaffold(model_id, options = {})
  options.assert_valid_keys(:class_name, :suffix)

  singular_name = model_id.to_s
  class_name    = options[:class_name] || singular_name.camelize
  plural_name   = singular_name.pluralize
  suffix        = options[:suffix] ? "_#{singular_name}" : ""

optionsは指定しないので、

singular_name
"outgo"
class_name
"Outgo"
plural_name
"outgos"
suffix
""

ということになります。次に、options[:suffix]は定義していないのでindexメソッドが定義されます。

def index
  list
end

その後、各種メソッドが定義されています。長いのでlistメソッドおよび関連メソッドだけ載せます。

def list
  @outgo_pages, @outgos = paginate :outgos, :per_page => 10
  render_scaffold "list"
end
def render_scaffold(action=nil)
  action ||= caller_method_name(caller)
  # logger.info ("testing template:" + "#{self.class.controller_path}/#{action}") if logger

  if template_exists?("#{self.class.controller_path}/#{action}")
    render :action => action
  else
    @scaffold_class = Outgo
    @scaffold_singular_name, @scaffold_plural_name = "outgo", "outgos"
    @scaffold_suffix = ""
    add_instance_variables_to_assigns

    @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/$/, "")), false))

    if !active_layout.nil?
      render :file => active_layout, :use_full_path => true
    else
      render :file => scaffold_path('layout')
    end
  end
end
def scaffold_path(template_name)
  File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end

以上をふまえた上でActionController::Base.processメソッドを見ていくことにしましょう。

ActionController::Base ($GEM_HOME/gems/actionpack/lib/action_controller/base.rb)

まず、ActionController::Base.processメソッドです。オブジェクトを生成して同名のインスタンスメソッドを呼び出しています。

def process(request, response) #:nodoc:
  new.process(request, response)
end

インスタンスメソッドの方のprocessです。

def process(request, response, method = :perform_action, *arguments) #:nodoc:
  initialize_template_class(response)
  assign_shortcuts(request, response)
  initialize_current_url
  assign_names
  forget_variables_added_to_assigns

  log_processing
  send(method, *arguments)

  assign_default_content_type_and_charset
  response
ensure
  process_cleanup
end

ActionController::Base#initialize_template_class

まず、initialize_template_classメソッドです。

def initialize_template_class(response)
  raise "You must assign a template class through ActionController.template_class= before processing a request" unless @@template_class

  response.template = self.class.view_class.new(self.class.view_root, {}, self)
  response.redirected_to = nil
  @performed_render = @performed_redirect = false
end

@@template_classはいつ設定されたんだ?と思ったところ、トップレベルのaction_controller.rbが読み込まれたときに設定されていました。

ActionController::Base.template_class = ActionView::Base

action_controller.rbを見たついでに、ActionControllerおよびActionView, ActiveRecordは様々なメソッドを別ファイルに定義しておいてBaseクラスにincludeするというスタイルのようです。追いにくいったらありゃしない

view_classメソッドです。

def self.view_class
  @view_class ||=
    # create a new class based on the default template class and include helper methods
    returning Class.new(ActionView::Base) do |view_class|
      view_class.send(:include, master_helper_module)
    end
  end

master_helper_moduleはhelper.rbで定義されています。ここではまだアプリケーションディレクトリにあるapp/helpers/*_helper.rbが取り込まれるわけではなさそうです。

次に、view_rootメソッドですがtemplate_rootメソッドを参照しています。これまたどこで設定されたんだろう?とgrepをかけたところ、Rails::Initializer#initialize_framework_viewsメソッドで設定されていました。

def initialize_framework_views
  for framework in ([ :action_controller, :action_mailer ] & configuration.frameworks)
    framework.to_s.camelize.constantize.const_get("Base").template_root ||= configuration.view_path
  end
end

view_pathはアプリケーションディレクトリ/app/viewsです。

ActionController::Base#assign_names

processメソッドに戻って、次のassign_shotcutsメソッドではその名の通りrequest, responseの各メンバーに対してのショートカットアクセス方法を設定しています。

次のinitialize_current_urlメソッドではUrlRewriterオブジェクトを生成しています。

次のassign_namesメソッドはまじめに見ましょう。先ほどコントローラを引き当てたときに設定されたアクションを設定しています。って、http://localhost:3000/accountbook/とアクセスするはずなのでparams['action']はnilです。結果、@action_nameとして'index'が設定されることになります。

def assign_names
  @action_name = (params['action'] || 'index')
end

ActionController::Base#perform_action

さて、perform_actionメソッドです。いろいろ条件分岐していますが一番上だけ見ればいいはずです。

def perform_action
  if self.class.action_methods.include?(action_name)
    send(action_name)
    render unless performed?

action_methodsはActionController::Baseのサブクラスで定義されたメソッドです。action_nameが'index'でscaffoldメソッドによりAccountbookControllerにはindexメソッドが定義されているのでindexメソッドが呼ばれることになります。

ActionView::Base.paginate ($GEM_HOME/gems/actionpack/lib/action_controller/pagination.rb)

さて、indexメソッドですがlistメソッドを呼んでいるだけなのでlistメソッドを再掲します。

def list
  @outgo_pages, @outgos = paginate :outgos, :per_page => 10
  render_scaffold "list"
end

paginateメソッドに進みましょう。定義されているファイルはセクションのところに書いてある通りです。

def paginate(collection_id, options={})
  Pagination.validate_options!(collection_id, options, true)
  paginator_and_collection_for(collection_id, options)
end

validate_options!メソッドはoptionsへのデフォルト値のマージ、collecion_idから生成される単数系名、クラス名を設定しています。Paginationが変更されるわけではないので!ではないようなという気がします。

次に、paginator_and_collection_forメソッドです。

def paginator_and_collection_for(collection_id, options) #:nodoc:
  klass = options[:class_name].constantize
  page  = params[options[:parameter]]
  count = count_collection_for_pagination(klass, options)
  paginator = Paginator.new(self, count, options[:per_page], page)
  collection = find_collection_for_pagination(klass, options, paginator)
  
  return paginator, collection 
end

options[:parameter]は'page'なので、リクエスト中のpage=nを取得することになりますが今回はnilです。

count_collection_for_paginationメソッドです。どうやらここからはActiveRecordに舞台が移るようです。

def count_collection_for_pagination(model, options)
  model.count(:conditions => options[:conditions],
              :joins => options[:join] || options[:joins],
              :include => options[:include],
              :select => options[:count])
end

ActiveRecord::Base

ActiveRecord::Base.count ($GEM_HOME/gems/activerecord/lib/active_record/calcuration.rb)

countメソッドです。

def count(*args)
  calculate(:count, *construct_count_options_from_legacy_args(*args))
end

続いてcalculateメソッドです。

def calculate(operation, column_name, options = {})
  validate_calculation_options(operation, options)
  column_name     = options[:select] if options[:select]
  column_name     = '*' if column_name == :all
  column          = column_for column_name
  catch :invalid_query do
    if options[:group]
      return execute_grouped_calculation(operation, column_name, column, options)
    else
      return execute_simple_calculation(operation, column_name, column, options)
    end
  end
  0
end

column_nameは'*'になります。そのため、columnは空配列です。column_forメソッド中でcolumnsメソッドが呼ばれてテーブルの列情報を取得しているようですが後で見ることにします。options[:group]は指定していないのでexecute_simple_calculationメソッドに進みます。それにしてもcatchが使われてるのって始めてみました:-)

def execute_simple_calculation(operation, column_name, column, options) #:nodoc:
  value = connection.select_value(construct_calculation_sql(operation, column_name, options))
  type_cast_calculated_value(value, column, operation)
end

construct_calculation_sqlメソッドは淡々とSQLを構築しています。それではデータベースとの通信部分に移りましょう。

ActiveRecord::Base.connection ($GEM_HOME/gems/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb)

connectionメソッドは前回も見たconnection_specification.rbに定義されています。

def connection
  if @active_connection_name && (conn = active_connections[@active_connection_name])
    conn
  else
    # retrieve_connection sets the cache key.
    conn = retrieve_connection
    active_connections[@active_connection_name] = conn
    end
  end
end

実際にデータベースと通信する必要があるまで接続しないという処理が行われています。続いてretrieve_connectionメソッドです。

def self.retrieve_connection #:nodoc:
  # Name is nil if establish_connection hasn't been called for
  # some class along the inheritance chain up to AR::Base yet.
  if name = active_connection_name
    if conn = active_connections[name]
      # Verify the connection.
      conn.verify!(@@verification_timeout)
    elsif spec = @@defined_connections[name]
      # Activate this connection specification.
      klass = name.constantize
      klass.connection = spec
      conn = active_connections[name]
    end
  end

  conn or raise ConnectionNotEstablished
end

接続はまだないのでelsifの方が実行されます。どうにも混乱するのですが、nameはActiveRecord::Baseです。なので、すぐ下にあるconnection=メソッドが呼ばれます(本当はquery_cache.rbでActiveRecord::Base.connection=が再定義されてそっち経由で呼ばれているようですが紛らわしいので)。

def self.connection=(spec) #:nodoc:
  if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
    active_connections[name] = spec
  elsif spec.kind_of?(ConnectionSpecification)
    config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
    self.connection = self.send(spec.adapter_method, config)
  elsif spec.nil?
    raise ConnectionNotEstablished
  else
    establish_connection spec
  end
end

specはConnectionSpecificationのためadapter_methodが呼ばれることになります。これで実際のDBMSとの接続が行われます。その結果をconnectionに代入して・・・読む身となっては:-<な書き方ですね。上のactive_connections[name] = specが実行されるので無限再帰ではありません。

ActiveRecord::Base.columns ($GEM_HOME/gems/activerecord/lib/active_record/base.rb)

それでは予告通りcolumnsメソッドです。

def columns
  unless @columns
    @columns = connection.columns(table_name, "#{name} Columns")
    @columns.each {|column| column.primary = column.name == primary_key}
  end
  @columns
end

table_nameメソッドに進みましょう。

def table_name
  reset_table_name
end
def reset_table_name #:nodoc:
  base = base_class

  name =
    # STI subclasses always use their superclass' table.
    unless self == base
      base.table_name
    else
      # Nested classes are prefixed with singular parent table name.
      if parent < ActiveRecord::Base && !parent.abstract_class?
        contained = parent.table_name
        contained = contained.singularize if parent.pluralize_table_names
        contained << '_'
      end
      name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
    end

  set_table_name(name)
  name
end

base_classメソッドはActiveRecord::Baseを直接継承しているクラスを返します。今回の場合はOutgoです。次に、parentはObjectなはずなのでifの中身は実行されずundecorated_table_nameメソッドに処理が移ります。

def undecorated_table_name(class_name = base_class.name)
  table_name = Inflector.underscore(Inflector.demodulize(class_name))
  table_name = Inflector.pluralize(table_name) if pluralize_table_names
  table_name
end

Inflectorの各メソッドは$GEM_HOME/gems/activesupport/lib/active_support/inflector.rbを参照してください。ともかくこれでクラスOutgoからデータを取得するテーブル名outgosが取得できました。

ActionView::Base

今までのところで表示するための情報は取得できた気がするので次は実際に表示している部分を見ることにしましょう。以下はscaffoldメソッドによって自動生成されるメソッドです。しばらく間が開いているので念のために書いておくとAccountbookControllerクラスに定義されています。

def render_scaffold(action=nil)
  if template_exists?("#{self.class.controller_path}/#{action}")
    render :action => action
  else
    @scaffold_class = Outgo
    @scaffold_singular_name, @scaffold_plural_name = "outgo", "outgos"
    @scaffold_suffix = ""
    add_instance_variables_to_assigns

    @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/$/, "")), false))

    if !active_layout.nil?
      render :file => active_layout, :use_full_path => true
    else
      render :file => scaffold_path('layout')
    end
  end
end
def scaffold_path(template_name)
  File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end

まず初めのif。アプリケーションディレクトリ/app/views/listなんとかはないのでelseに進みます。なんとかと言っているのはちゃんとわけがあるので興味がある方はご覧ください。

その後いろいろ設定を行い、デフォルトのテンプレートを使ってページの生成を行っています。なお、active_layoutはnilなのでelseの方が実行されます。

ActionView::Base#render_file ($GEM_HOME/gems/actionpack/lib/action_view/base.rb)

それではrender_fileメソッドです。渡される引数を考慮すると以下の部分が実行されます。local_assignsは第3引数でデフォルトは空ハッシュです。

template_file_name = template_path
template_extension = template_path.split('.').last
template_source = nil # Don't read the source until we know that it is required
render_template(template_extension, template_source, template_file_name, local_assigns)

render_templateメソッドです。

def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc:
  if handler = @@template_handlers[template_extension]
    template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded.
    delegate_render(handler, template, local_assigns)
  else
    compile_and_render_template(template_extension, template, file_path, local_assigns)
  end
end

template_handlerを登録した覚えはないのでelseのcompile_and_render_templateメソッドに進みましょう。

def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {}) #:nodoc:
  # convert string keys to symbols if requested
  local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys

  # compile the given template, if necessary
  if compile_template?(template, file_path, local_assigns)
    template ||= read_template_file(file_path, extension)
    compile_template(extension, template, file_path, local_assigns)
  end

  # Get the method name for this template and run it
  method_name = @@method_names[file_path || template]
  evaluate_assigns

  send(method_name, local_assigns) do |*name|
    instance_variable_get "@content_for_#{name.first || 'layout'}"
  end
end

初回ということでcompile_templateメソッドに進みます。長いので通らないところとか例外処理とか省略。

def compile_template(extension, template, file_name, local_assigns)
  render_symbol = assign_method_name(extension, template, file_name)
  render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)

  line_offset = @@template_args[render_symbol].size

  CompiledTemplates.module_eval(render_source, file_name, -line_offset)

  @@compile_time[render_symbol] = Time.now
end

assign_method_nameメソッドはファイル名などからキーを生成しています。

次のcreate_template_sourceメソッドは・・・また変なことをやらかしてますね。例によって通らないところ省略です。

def create_template_source(extension, template, render_symbol, locals)
  body = ERB.new(template, nil, @@erb_trim_mode).src

  @@template_args[render_symbol] ||= {}
  locals_keys = @@template_args[render_symbol].keys | locals
  @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }

  locals_code = ""
  locals_keys.each do |key|
    locals_code << "#{key} = local_assigns[:#{key}]\n"
  end

  "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end

ふうむ、上のメソッドで何でsendなんてしてるんだろうと思ったらまた動的にメソッド書いてるんですね。ともかくこれでページが書かれました。

おわりに

今回はRuby on Railsのうち、リクエストが来たときにどのような処理が行われるかを見ていきました。感想としては、

  • これぐらいの規模になると、ある設定がどこでされてるのかが把握しづらくなる
  • オンデマンド設定が加わってくると一層わかりづらくなる
  • 動的に書かれるコードが多い

といったところです。

それではみなさんもよいコードリーディングを。


トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-08-19 (日) 03:58:06 (4502d)