「コミッターと読み進めるRailsリーディング会 #1」を開催しました!~ Rails v1.0.0を読み進める! ~

はじめに

はじめまして!オクトのRailsエンジニアの @KanechikaAyumu です!

弊社では、日々色々な勉強会が開催されています。

先日は、ANDPADの技術顧問をして頂いている松田さんにRailsリーディング会の勉強会を開催して頂きました!

prtimes.jp

貴重なお話を聞ける機会なので、社内の勉強会に閉じるのがもったいないと思い、外部の方も参加できるような形式を取らせて頂きました。当日参加してくださった方、平日の日中のお忙しい中にご参加頂き、ありがとうございました!!

oct.connpass.com

Railsソースコードを読んだことない人や、どこから読み進めて良いのか分からずな方は、参考にして頂ければと思います!

  1. 当日の様子
  2. Rails v1.0.0
  3. コミッター紹介
  4. Active Record
  5. Action Pack
  6. alias_method
  7. 最後に

当日の様子

当日は社内のエンジニアに加えて、数名外部のエンジニアの方にご参加頂きました。ご参加ありがとうございます!

松田さんの画面操作を見ながら、参加者全員でリーディングを行いました。Railsだけではなく、ソースコードの読み方やコマンド操作などたくさん学ぶことができました!

f:id:ayumu-kanechika:20200216175542j:plain

f:id:ayumu-kanechika:20200216175608j:plain


Rails v1.0.0

松田さんのオススメのOSSの読み方は、「First Commitから読み進めていくこと」とのことです。作者の本当に実現したかった内容が、ノイズ等がなく、シンプルに書き下されている為です。今回の勉強会でもv1.0.0から読み進めて行くことになりました。
 
皆さんもお手元にチェックアウトしてみて下さい!
$ git clone git@github.com:rails/rails.git
$ cd rails
$ git checkout v1.0.0
 

Railsは、フルスタックのMVCフレームワークです。
Mのモデルレイヤーは、O/Rマッパーも兼ねて「Active Record」が担います。
Cのベースとしては「「Action Pack」があり、当時は「ActionController」や「ActionView」が担っておりました。MVCの糊付けとして、「Railtise」があります。

勉強会では、中核な処理の「Active Record」「Action Pack」を読み進めました。

コミッター紹介 

その前にコミッター紹介です。生産者の顔が一番大事とのことです!

 

Railsは、DHHのファーストコミットで、大枠は出来上がっていました。
Action Mailer、Action Pack、Active Record、RailtiseなどMVCの中核の部分はだいたい全部入っていました。2004年にBase Camp(当時、37signals)のWebApplicationから切り出した為です。(とある1つ企業のフレームワークの切り出しで、この設計力の高さは驚愕です!!)
 
その最初のコミットがこちら
Initial
綺麗なコミットが積まれているのは、DHHがそういう性格の人だそうです。
 他にもコミットやRails Contributors をなどを見ながら、名前の挙がった方をつらつらと記載します!
$ git shortlog -sn db045dbbf6..v1.0.0
 
1771 David Heinemeier Hansson
278 Jeremy Kemper
205 Jamis Buck
79 Nicholas Seckar
77 Leon Breedt
70 Marcel Molina
42 Sam Stephenson
14 Thomas Fuchs
13 Tobias Lütke
12 Scott Barron
9 Florian Weber
8 Michael Koziarski
  1. DHH(David Heinemeier Hansson
    ファーストオーサー 37 signals CTO
    当時では珍しいRubyを使って、プロダクトを作り始めた。
    当時25歳。コードが書けて、設計できて、イケメン!!

  2. Jeremy Kemper
    No.2の重要人物
    Base Campにあとからジョインした。
    20世紀の頃からRubyを触っていた。
    Railsを影から支えるNo.2
    今の名前は、Jeremey Daer。
    結婚して、夫婦の名字を足して2で割ったらしい

  3. Jamis Buck
    Base campに後からジョイン
    代表作は、Switch Tower(?)。Rubyでピッとデプロイができる代物。
    商標などの兼ね合いで、のちにCapistranoに変わる。

  4. Nicholas Seckar
    主にActiveRecordを開発

  5. Marcel Molina
    当時は最年少。シンボルにProcを導入!

  6. SamStephenson
    まだ尚、社員
    JS系を色々と作っている。
    Asset Pipelineを作った人。Sprocketsも。

  7. Tobias
    社外の方。MySQLに強い。カナダ人。ActiveRecord
    Shopifyを起業をして、「CTO」ではなく「CEO」をやっている。

  8. Michael Koziarski
    ニュージーランドRailsコミッター。セキュリティ周り

  9. Eric Hodel
    シアトルRubyの親玉

  10. JoshPeek
    Railsコミッター。Rack。
    サンフランシスコの小さい企業Githubを立ち上げた。
    Rails魔改造をし続けて、Githubが2系を使い続けていた。

  11. Chat Fowler
     Rubyconf

  12. Shugo Maeda
    Railsのv1.0.0で唯一コミットが取り込まれた日本人

 

Active Record

当時のv1.0.0では「activerecord/lib/active_record/base.rb」の約1800行にコアの処理が全て書かれていました。他はおまけとのこと!
 
Railsも当時は新出のフレームワークだったので、ソースコードのコメントもかなり丁寧に書かれているので、キャッチアップしやすいです。
 
findメソッドにinteger渡すとプライマリキーとして検索されるなど、既に今と同じ形式でした。 機能としては、大枠できており、最先端という感じでした。最近の開発では、DSLが増えているという感じ。
 当時はfindしか使っておらず、relationとかなかったので、即SQL発行がされていました。
 
#find が面白いので、読み進めていきます!
def find(*args)
  ...
  case args.first
    when :first
      find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first
    when :all
      records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options))
      ...
    else
      ...
      conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
 
引数にfirst、all、それ以外の何を渡しても最終的に「find_by_sql」に「construct_finder_sql」が呼び出されています。
 
#construct_finder_sql はストレートに処理が書かれています!
def construct_finder_sql(options)
  sql = "SELECT #{options[:select] || '*'} FROM #{table_name} "
  add_joins!(sql, options)
  add_conditions!(sql, options[:conditions])
  sql << " GROUP BY #{options[:group]} " if options[:group]
  sql << " ORDER BY #{options[:order]} " if options[:order]
  add_limit!(sql, options)
  sql
end
 
#find_by_sql は今でも残る重要なAPIとのことです!
def find_by_sql(sql)
  connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
 
次に、#save を読み進めていきます。
def save
  raise ActiveRecord::ReadOnlyRecord if readonly?
  create_or_update
end
 
中では、#create#update を呼び出すことになり、そちらも愚直にSQLを構築しています!
def create
  if self.id.nil? and connection.prefetch_primary_key?(self.class.table_name)
    self.id = connection.next_sequence_value(self.class.sequence_name)
  end
  self.id = connection.insert(
    "INSERT INTO #{self.class.table_name} " +
    "(#{quoted_column_names.join(', ')}) " +
    "VALUES(#{attributes_with_quotes.values.join(', ')})",
    "#{self.class.name} Create",
    self.class.primary_key, self.id, self.class.sequence_name
  )
  @new_record = false
end
 本当にRailsのコアの処理はv1.0.0に詰まっています!
 
他に大事なのはなどがあるのですが、時間が足りずなので、次回以降のお楽しみになりました!

Action Pack

なぜ「Action Pack」という名前かは不明だが、「Action Controller」、「Action VIiew」を「Pack」したものだから?など諸説ありです。

こちらもコアなロジックは「 actionpack/lib/action_controller/base.rb 」にまとまっています。

 当時は、Rackがまだありませんでしたので、CGIベースで、request、response、cookieなどがありました。
また、今との大きな違いで、RESTfulの概念が入る前だったので、アクション名は自由でした。

#     def sign
#       Entry.create(params[:entry])
#       redirect_to :action => "index"
#     end
 
今では当たり前のパラメータ渡しを発明したのもDHHです!
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
# <input type="text" name="post[name]" value="david">
# <input type="text" name="post[address]" value="hyacintvej">
  
Action Controllerと Action View側でオブジェクト違うが、インスタンス変数を共有していることが「It's automatically configured.」とすらっとコメントに記述されています。
細かい挙動を読み進めていきました。
# == Renders
#
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
# The controller passes objects to the view by assigning instance variables:
#
#   def show
#     @post = Post.find(params[:id])
#   end
 
 当時は、WEBrickで作られたカスタムのサーブレットでした。

railties/lib/webrick_server.rb 

# A custom dispatch servlet for use with WEBrick. It dispatches requests
# (using the Rails Dispatcher) to the appropriate controller/action. By default,
# it restricts WEBrick to a managing a single Rails request at a time, but you
# can change this behavior by setting ActionController::Base.allow_concurrency
# to true.
class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet

 

 Responseはどこで作られているのか読み進めていきます。

 
railties/lib/dispatcher.rb

# Dispatch the given CGI request, using the given session options, and
# emitting the output via the given output.  If you dispatch with your
# own CGI object be sure to handle the exceptions it raises on multipart
# requests (EOFError and ArgumentError).
def dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
  if cgi ||= new_cgi(output)
    request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
    prepare_application
    ActionController::Routing::Routes.recognize!(request).process(request, response).out(output)
  end
rescue Object => exception
  failsafe_response(output, '500 Internal Server Error') do
    ActionController::Base.process_with_exception(request, response, exception).out(output)
  end
ensure
  # Do not give a failsafe response here.
  reset_after_dispatch
end

 

リクエストのほぼ全ての処理をこの1行でやっていました。
ActionController::Routing::Routes.recognize!(request).process(request, response).out(output)」

ざっくりと

  1. ActionController::Routing::Routes.recognize!(request)」
    あらかじめ準備しているルーティングテーブルを元に、URLとパラメーターからアクションを特定。

  2. process(request, response)
    コントローラーのアクションメソッドを呼び出す。リクエストを渡して、レスポンスに値を格納する

  3. out(output)
    Viewをレンダリングして、文字列を返す。

1. ActionController::Routing::Routes.recognize!(request)

まずはこちらから読み進めていきます!

actionpack/lib/action_controller/routing.rb

def recognize(request)
  string_path = request.path  
  string_path.chomp! if string_path[0] == ?/  
  path = string_path.split '/'  
  path.shift  
 
  hash = recognize_path(path)  
  return recognition_failed(request) unless hash && hash['controller']  
 
  controller = hash['controller']  
  hash['controller'] = controller.controller_path  
  request.path_parameters = hash  
  controller.new
end
alias :recognize! :recognize

 戻りが衝撃の「controller.new」 

2. process(request, response)

# Extracts the action_name from the request parameters and performs that action.
def process(request, response, method = :perform_action, *arguments) #:nodoc:
  initialize_template_class(response)
  assign_shortcuts(request, response) 
  initialize_current_url
  @action_name = params['action'] || 'index'
  @variables_added = nil
 
  log_processing if logger
  send(method, *arguments)
  @response
ensure
  close_session
end

 

ユーザーが触るparamsとかはここで準備されています。 

def assign_shortcuts(request, response)
  @request, @params, @cookies = request, request.parameters, request.cookies

  @response = response
  @response.session = request.session

  @session = @response.session
  @template = @response.template
  @assigns = @response.template.assigns
  @headers = @response.headers
end

 

 後続の「render」処理から呼び出されて、instance_variablesから「@assigns」に格納されます。

def add_instance_variables_to_assigns
  @@protected_variables_cache ||= protected_instance_variables.inject({}) { |h, k| h[k] = true; h }
  instance_variables.each do |var|
    next if @@protected_variables_cache.include?(var)
    @assigns[var[1..-1]] = instance_variable_get(var)
  end
end  

3. out(output)

def out(output = $stdout)
  convert_content_type!(@headers)
  output.binmode      if output.respond_to?(:binmode)
  output.sync = false if output.respond_to?(:sync=)
  
  begin
    output.write(@cgi.header(@headers))
 
    if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
      return
    elsif @body.respond_to?(:call)
      @body.call(self, output)
    else
      output.write(@body)
    end
 
    output.flush if output.respond_to?(:flush)
  rescue Errno::EPIPE => e
    # lost connection to the FCGI process -- ignore the output, then
  end
end
 
View側からは、先ほどcontroller側で格納した「@assigns」から値を取り出すことで、参照できるようになっています
def evaluate_assigns
  unless @assigns_added
    assign_variables_from_controller
    @assigns_added = true
  end
end

Routing は、今の機構と全然異なる為、次の機会のお楽しみとなりました!

alias_method

松田さん的な見所とのことです!
aliasを2度呼んで、パッチしているこの発想力が当時、素晴らしかった。
Module#alias_method_chain
alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout
 
publicメソッドが残っていまい、ユーザー側からも呼び出せてしまうので、現在では、別の方法が採用されているが、Rails 3、4などを読む上では頻出とのことで、要チェックです!

最後に

今回はv1.0.0を読み進めることで、コアの処理を集中して、読み進めることができました。次回は、v1からv2で、Restfulの概念の導入など色々と理解できればと思います!
 
今回のリーディング会では、Railsをどんな人たちが作っているのかなどを一番伝えたかったとのことでした!
 
個人の所感としては、社内のフレームワークから、Webサービスとしてのコアな処理の部分をOSSとして抜き出したとのことでしたが、当時の20代中盤のDHHが作ったプロダクトの設計力の高さに驚愕しました!!
 
引き続き一流のソースコードを読み進めていければと思います!