はじめに
はじめまして!オクトのRailsエンジニアの @KanechikaAyumu です!
弊社では、日々色々な勉強会が開催されています。
先日は、ANDPADの技術顧問をして頂いている松田さんにRailsリーディング会の勉強会を開催して頂きました!
prtimes.jp
貴重なお話を聞ける機会なので、社内の勉強会に閉じるのがもったいないと思い、外部の方も参加できるような形式を取らせて頂きました。当日参加してくださった方、平日の日中のお忙しい中にご参加頂き、ありがとうございました!!
Railsのソースコードを読んだことない人や、どこから読み進めて良いのか分からずな方は、参考にして頂ければと思います!
- 当日の様子
- Rails v1.0.0
- コミッター紹介
- Active Record
- Action Pack
- alias_method
- 最後に
当日の様子
当日は社内のエンジニアに加えて、数名外部のエンジニアの方にご参加頂きました。ご参加ありがとうございます!
松田さんの画面操作を見ながら、参加者全員でリーディングを行いました。Railsだけではなく、ソースコードの読み方やコマンド操作などたくさん学ぶことができました!
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つ企業のフレームワークの切り出しで、この設計力の高さは驚愕です!!)
その最初のコミットがこちら
綺麗なコミットが積まれているのは、DHHがそういう性格の人だそうです。
$ 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
- DHH(David Heinemeier Hansson)
ファーストオーサー 37 signals CTO
当時では珍しいRubyを使って、プロダクトを作り始めた。
当時25歳。コードが書けて、設計できて、イケメン!!
- Jeremy Kemper
No.2の重要人物
Base Campにあとからジョインした。
20世紀の頃からRubyを触っていた。
Railsを影から支えるNo.2
今の名前は、Jeremey Daer。
結婚して、夫婦の名字を足して2で割ったらしい
- Jamis Buck
Base campに後からジョイン
代表作は、Switch Tower(?)。Rubyでピッとデプロイができる代物。
商標などの兼ね合いで、のちにCapistranoに変わる。
- Nicholas Seckar
主にActiveRecordを開発
- Marcel Molina
当時は最年少。シンボルにProcを導入!
- SamStephenson
まだ尚、社員
JS系を色々と作っている。
Asset Pipelineを作った人。Sprocketsも。
- Tobias
社外の方。MySQLに強い。カナダ人。ActiveRecord。
Shopifyを起業をして、「CTO」ではなく「CEO」をやっている。
- Michael Koziarski
ニュージーランドのRailsコミッター。セキュリティ周り
- Eric Hodel
シアトルRubyの親玉
- JoshPeek
Railsコミッター。Rack。
サンフランシスコの小さい企業Githubを立ち上げた。
Railsの魔改造をし続けて、Githubが2系を使い続けていた。
- Chat Fowler
Rubyconf
- Shugo Maeda
Railsのv1.0.0で唯一コミットが取り込まれた日本人
Active Record
Railsも当時は新出のフレームワークだったので、ソースコードのコメントもかなり丁寧に書かれているので、キャッチアップしやすいです。
findメソッドにinteger渡すとプライマリキーとして検索されるなど、既に今と同じ形式でした。 機能としては、大枠できており、最先端という感じでした。最近の開発では、DSLが増えているという感じ。
当時はfindしか使っておらず、relationとかなかったので、即SQL発行がされていました。
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」が呼び出されています。
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
def find_by_sql(sql)
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
def save
raise ActiveRecord::ReadOnlyRecord if readonly?
create_or_update
end
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)」
ざっくりと
- ActionController::Routing::Routes.recognize!(request)」
あらかじめ準備しているルーティングテーブルを元に、URLとパラメーターからアクションを特定。
- process(request, response)
コントローラーのアクションメソッドを呼び出す。リクエストを渡して、レスポンスに値を格納する
- 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_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が作ったプロダクトの設計力の高さに驚愕しました!!
引き続き一流のソースコードを読み進めていければと思います!