Railsにコントリビュートした
軽い気持ちでpull requestを送ってみたら数十分後になんとmergeされてしまった。間違ってmasterブランチに送ってしまったため、おそらくrails 5で公開されることになる。
追加したのはrake initializer
という簡単なrakeタスクで、railsの起動時に実行されるinitializerを実行順に出力する。
% rake initializer set_load_path set_load_path set_load_path set_load_path set_autoload_paths set_autoload_paths set_autoload_paths set_autoload_paths add_routing_paths add_routing_paths add_routing_paths add_routing_paths add_locales add_locales add_locales add_locales add_view_paths add_view_paths add_view_paths add_view_paths load_environment_config load_environment_config load_environment_config load_environment_config load_environment_hook load_active_support set_eager_load initialize_logger initialize_cache initialize_dependency_mechanism bootstrap_hook active_support.deprecation_behavior prepend_helpers_path prepend_helpers_path prepend_helpers_path prepend_helpers_path load_config_initializers load_config_initializers load_config_initializers load_config_initializers active_support.halt_callback_chains_on_return_false active_support.initialize_time_zone active_support.initialize_beginning_of_week active_support.set_configs action_dispatch.configure active_model.secure_password action_view.embed_authenticity_token_in_remote_forms action_view.logger action_view.set_configs action_view.caching action_view.collection_caching action_view.setup_action_pack action_controller.assets_config action_controller.set_helpers_path action_controller.parameters_config action_controller.set_configs action_controller.compile_config_methods active_record.initialize_timezone active_record.logger active_record.migration_error active_record.check_schema_cache_dump active_record.set_configs brancher.rename_database active_record.initialize_database active_record.log_runtime active_record.set_reloader_hooks active_record.add_watchable_files global_id active_job.logger active_job.set_configs action_mailer.logger action_mailer.set_configs action_mailer.compile_config_methods setup_sass setup_compression jbuilder web_console.initialize web_console.insert_middleware web_console.templates_path web_console.whitelisted_ips web_console.whiny_requests web_console.acceptable_content_types engines_blank_point engines_blank_point engines_blank_point engines_blank_point append_assets_path append_assets_path append_assets_path turbolinks append_assets_path add_generator_templates ensure_autoload_once_paths_as_subset add_builtin_route build_middleware_stack define_main_app_helper add_to_prepare_blocks run_prepare_callbacks eager_load! finisher_hook set_routes_reloader_hook set_clear_dependencies_hook disable_dependency_loading
もともとは、naoty/brancherというrubygemを作るときに、railsの初期化プロセスにコードを差し込みたくてinitializerが実行される順番を確認するデバッグ用のコードを書いていたのがきっかけだった。似たようなrakeタスクにrake middleware
というものがあり、これがあるならrake initializer
もあっていいだろうという軽い気持ちでpull requestを送ってみた。
意外とすんなりmergeしてもらったので、railsへのコントリビュートに対して心理的なハードルがかなり低くなった。上の出力を見てもらえればわかるとおり同じようなinitializerが実行されており、起動が遅くなったり、initializerで定数を定義したときに大量のwarningが出たりする。前々から定数のwarningが大量に出る問題は不思議に思っていたが、これが原因なのかもしれない。またコントリビュートするとしたら、ここらへんを解決するところになりそう。
Rubotyで勤務時間を管理する
リモートワークのときでも勤務時間を自動的に記録するため、botに発言時間を監視させ、だいたいの勤務時間を記録させるようにした。
いま使っているbotはr7kamura/ruboty製で、これに機能を追加するプラグインを書いた。
このプラグインは発言者ごとに発言時間を記録する。
このプラグインは勤務時間のストレージを提供する。既存のストレージにはr7kamura/ruboty-redisがある。これを利用する場合、保存された勤務時間を取得するコマンドを用意する必要がある。それを非エンジニアに理解してもらうのは厳しいと思ったので、直感的に理解できるGoogle Spreadsheetをストレージとして利用できるようにするプラグインを作った。
ソースコードを読むとわかるけど、この2つのプラグインは密結合しているため、設計上はいい出来とは言えない。ruboty-timecard内でruboty-google_spreadsheetを使うことを想定したアクセスの仕方をしている。rubotyのストレージをうまく抽象化するインターフェイスがあると解決しそうだが、なかなか難しい問題だと思う。
これら2つのプラグインは共に使われることを想定しているため、勤務時間を記録する用のテンプレートを作った。Herokuボタンから簡単にデプロイできる。
Timepieceを0.2.0にアップデートした
Swiftの日付操作ライブラリであるTimepieceに機能を追加し0.2.0にアップデートした。また、1ヶ月くらい前からしれっとCocoaPods(現在はまだrc版)をサポートしたのでCocoaPodsからインストールできるようになってる。
NSDate <-> Stringの変換
let timestamp = 3.years.ago.stringFromFormat("yyyy-MM-dd") XCTAssertEqual(timestamp, "2012-03-01", "") if let birthday = "1987-06-02".dateFromFormat("yyyy-MM-dd") { XCTAssertEqual(birthday.year, 1987, "") XCTAssertEqual(birthday.month, 6, "") XCTAssertEqual(birthday.day, 2, "") }
NSDateFormatter
をいちいち初期化するのが面倒だったので、これを内部的に呼び出す直感的なメソッドを追加した。フォーマット文字列は同じ。
オーバーロードの追加
Before:
After:
これまではNSDate.date(year:month:day:hour:minute:second:)
はhour
, minute
, second
のデフォルト引数を0
にセットしていたが、これではBeforeのスクショの通り補完候補にhour
, minute
, second
も含まれてしまう。デフォルト引数を使うよりも引数を省略したオーバーロードを提供した方が、補完候補に省略版のメソッドが現れるため使いやすいと思う。
今後の予定
NSDate
同士を比較演算子で比較できるようにしたい。1.5.hour
のようなFloat
のサポート(けっこう難しそう)
「UMLモデリングの本質」を読んだ
- 作者: 児玉公信
- 出版社/メーカー: 日経BP社
- 発売日: 2011/05/26
- メディア: 単行本
- 購入: 6人 クリック: 23回
- この商品を含むブログ (6件) を見る
「UMLモデリングの本質」という本を読んだ。最近、ソフトウェアの設計について興味があって、いろいろ調べてみたところ各所でこの本がオススメされていたので手にとってみた。以前のエントリー(「達人に学ぶDB設計徹底指南書」を読んだ - naoty.to_s)でデータベースの設計について理解できたんだけど、結局のところ、そのシステムが扱う業務内容をいかにして実装可能なモデルに落とし込むか(=モデリング)が重要になってくると思う。この本で理解したかったのはそこだった。タイトルからUMLの書き方についての本のようにも思えるが、そうではなく、むしろまったくUMLの書き方は書いてない。UMLを使って、複雑な業務用件をいかにモデリングするかを説いている。本書は300ページに満たないものの、密度が非常にあり2, 3週間でゆっくり読んでも第3章までしか読めていない。それでも、十分に学びがあったので忘れてしまう前にエントリーとして残しておきたいと思う。
モデリングとは
モデリングとは「仕組みや概念を理解するために概念的な要素とそれらの関係を記述すること」とあった。これは普段の業務で多かれ少なかれ必ず行っていると思う。ただ、それをモデリングという工程として認識してはいなかった。そして、モデリングの手法についても特に考えることはなかった。いつも漫然とノートに四角形と線を書いて整理してた(以下、イメージ)。
こういう自己流ではなくて、標準的な概念の表記法がある。その1つがUMLだ。UMLを習得することで、他人とのコミュニケーションの手段として使えるようになるし、UMLを通して標準的なモデリングの手法も学べるようになる。
本書によると、モデリングは大まかに以下の順番に行っていくようだ。
- 業務フロー図を書いて業務フローを整理する。
- 業務フローからユースケース図を書いてユースケースを洗い出す。
- ユースケースから概念となる名詞を抜き出し、初期の型図を作成する。型図というのは実際はクラス図のことで、クラスと言っちゃうとクラスとして実装されることを含意してしまうが、実際にはクラスとは限らないため型図という言い方をしている。
- ユースケースごとにシーケンス図を書く。これによって各アクターの責務が明確になり、初期の型図を機能的側面から修正することができる。
UMLはけっこうな種類があるような気がするけど、とりあえず上に出てきた図だけ覚えておけばよさそうだ。各ステップの具体的な方法については本書を参照してほしい。読んだだけではあまり意味がないと思うので、近いうちに簡単な例で実践してみたいと思っている。
分類の実装
モデリングの手法の次はより実装に近い話が続く。その中でも分類の実装の話がよかった。例えば、ユーザーに「有料会員」と「無料会員」という分類がある場合に、それをフラグとして実装するのか「Stateクラス」として実装するのかという議論がある。
# フラグで実装 class User < ActiveRecord::Base enum plan: %i(free premium) end
# Stateクラスで実装 class User < ActiveRecord::Base has_one :plan end class Plan < ActiveRecord::Base end class Free < Plan end class Premium < Plan end
個人的にはStateクラスという手法を知らなかったので、いつも前者のフラグとして実装していた。この実装の問題点は、状態が増えた場合にコードを修正する必要がある点と、if文の分岐を多用することになりコードが複雑になってしまう点がある。特に後者は、状態フラグの種類(例えば、「公開アカウント」or「非公開アカウント」)が増えたときに組み合わせが倍になり指数関数的に複雑になってしまうため、重大な問題点だと思う。
Stateクラスであれば、サブクラスを追加するだけでよく既存のコードを修正する必要がない。if文による分岐もダックタイピングによって解決する。状態フラグが複数になった場合は、直交する状態をサブクラスとして定義する(PublicFree
, PrivatePremium
など)のがよいと本書では書かれていた。状態によって振る舞いが異なる場合や状態が増える可能性がある場合は、フラグではなくStateクラスで実装する方がいいのかもしれない。
ファサード
モデリングによって整理された概念をクラスとして実装する際、層別化アーキテクチャを使うのがいいという話が出てくる。よく知られた4層モデルの話で、システムを「ユーザーインターフェイス層」「アプリケーション層」「ドメイン層」「永続層」に分離し、層間の参照を一方向にすることで結合度を抑えるというアプローチだ。このとき、モデリングによって整理された概念はそのままドメイン層に配置することになる。そして、アプリケーション層には業務フローで定義された手続きや処理の手順を定義していくことになる。
アプリケーション層を実装していくなかで、ドメイン層に定義された概念が高度に抽象化されていて扱いづらいときがある。とは言え、これまでのモデリングで練り上げてきた概念をアプリケーション層の都合でねじまげるわけにはいかないだろう。そこで登場するのがファサードというオブジェクトだ。ファサードはアプリケーション層からは冗長に見えるドメインを1つにまとめて扱いやすくする。さらに、複数のドメインにまたがった排他制御も扱う。
ファサードという概念はなんとなく耳にしたことがあったが、モデリング→実装という流れの文脈で捉えるとその必要性を実感することができた。普段RailsでMVCだけを書いていると、こうした視点が持てずにドメインをねじ曲げてしまうことがある。なので、ファサードの実装方法について調べて実践していきたいと思った。
「達人に学ぶDB設計徹底指南書」を読んだ
達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ
- 作者: ミック
- 出版社/メーカー: 翔泳社
- 発売日: 2012/03/16
- メディア: 単行本(ソフトカバー)
- 購入: 21人 クリック: 316回
- この商品を含むブログ (23件) を見る
「達人に学ぶDB設計徹底指南書」を読んだ。最近、Webアプリケーションを開発する中で設計に問題意識を感じるようになった。ただ、具体的にどこが問題なのかうまく言語化できなくて、というか設計という言葉が何を指しているのかイマイチ分かってなかったので、データベースの設計について本を読んでみた。
内容
- データベースの設計は「論理設計」→「物理設計」の順に行う。論理設計はデータの要素やデータ間の関係を定める工程を指す。物理設計はデータを格納する物理的な領域や格納方法を定める工程を指す。
- 論理設計は「エンティティの抽出」→「エンティティの定義」→「正規化」→「ER図の作成」の順に行う。エンティティというのは業務内容に現れるデータの集合体を指す。
- 物理設計は「テーブル定義」→「インデックス定義」→「ハードウェアのサイジング」→「ストレージの冗長構成決定」→「ファイルの物理配置決定」の順に行う。
- 正規化というのはデータの形式を正規形にすることであり、正規形というのはデータの冗長性が排除され一貫性と効率性が保持されるデータ形式のこと。異なるエンティティを異なるテーブルに分離することによって第3正規形までは実現できる。
- 正規化の目的はデータの冗長性を排除して不整合な更新を避けることにある。ただし、正規化はテーブルを分離するため、SQLでjoinを使うことになる。joinはパフォーマンスが悪化するため、正規化とパフォーマンスはトレードオフの関係にある。
- 「非正規化は最後の手段」というのが筆者の意見である。
感想
- データベース設計で最初に行うのは「エンティティの抽出」であるということがとても重要なポイントに感じた。
- いつもRailsでアプリケーションを開発するとき、正規化を意識することはない。Modelを定義することはテーブルを定義することと同義だ。なので、適切にエンティティを定義できていれば、意識せずとも第2正規形、第3正規形を実現できると思う。筆者の「非正規化は最後の手段」という立場にたつと、いつもどおりModelを定義することで正規化を行い、非正規化以外の手段でパフォーマンスを向上させていけばいいということになる。つまり、今まで通りでいいってことか。
- 結局、「いかにしてエンティティを抽出・定義するか」という問題を考える必要がありそうだ。
ブランチごとにDB切り替えるヤツ作った
Gitのブランチ名をもとにActiveRecordが接続するDBを切り替えるRubygemを作った。
使い方
group :development do gem "brancher" end
Gemfileに書いてbundle install
するだけ。あとは自動的にブランチごとに別々のDBが使われるようになる。
config/database.ymlでdevelopment環境のDB名をsample_app_dev
と指定していた場合、master
ブランチならsample_app_dev_master
が使われるし、some_feature
ブランチならsample_app_dev_some_feature
が使われる。
問題意識
複数のブランチを移りながら開発していると、migrationを実行したブランチとしてないブランチでDBのスキーマが不整合になってエラーをおこすことがよくある。そのたびにrake db:migrate
だったりrake db:reset
だったりrake db:schema:load
したりするのが非常に面倒だった。そういった問題を解決するためにブランチごとにDBを分けられるツールを作った。
どう実現しているか
やっていることはconfig/database.ymlをロードしたオブジェクトをいじっているだけ。これをいじるタイミングは2つある。Railsアプリケーションの初期化時とdb:load_config
タスクだ。
Railsアプリケーションを初期化する際、ActiveRecord::Base
がロードされたあとにestablish_connection
が実行される。このメソッドはconfig/database.ymlに基いてDBとのコネクションを接続するものなので、これが実行される前にDB名をブランチ名に従っていじってあげる必要がある。実際に実行されているコードは以下の通りだ。
# lib/active_record/railtie.rb initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration begin establish_connection # ... end end end
Rails.application.config.database_configuration
はconfig/database.ymlの中身をERBで展開してYAMLとしてロードしたHashオブジェクトだ。これがself.configurations
にセットされてestablish_connection
が実行される(ここでのself
はActiveRecord::Base
)。よって、この初期化処理が実行される前にRails.application.config.database_configuration
をいじればいい。
初期化処理の一連の流れに割り込むにはRails::Initializable.initializer
メソッドのオプションを使う。そして、その中でRails.application.config.database_configuration
の中身を上書きする。
# lib/brancher/railtie.rb initializer "brancher.rename_database", before: "active_record.initialize_database" do Rails::Application::Configuration.send(:prepend, DatabaseConfigurationRenaming) end
次に、db:load_config
タスク内でもconfig/database.ymlをいじる必要がある。なぜかというと、rake db:create
などの一部のRakeタスクは上述の初期化処理が実行されないからだ。environment
タスクに依存しているタスクであれば、environment
タスク内で初期化処理が行われるため問題ない。一方、db:load_config
タスクは(おそらく)すべてのDBに関連するRakeタスクが依存しているため、ここでDB名をいじってあげればいい。
# lib/brancher/railtie.rb rake_tasks do namespace :db do task :load_config do DatabaseRenameService.rename!(ActiveRecord::Base.configurations) end end end
Rakeタスクは通常のメソッドとは異なり、同名のタスクを定義しても上書きされることはない。先に定義された順に同名のタスクが実行される。
所感
上のような初期化処理の仕組みやRakeタスクの追加などは以前のエントリなどでRailsの内部を読み理解を深めることによって実現することができた。ブラックボックスの中身が見えてくると、こういったRails自体に関わる便利ツールを簡単に作ることができてしまう。引き続きRailsのソースコードリーディングは続けていきたい。
`rails s`読んだ
rails s
でRailsサーバーが起動するまでに何が起きているのかを紐解くことでRailsとは何なのかを理解していきたい。今回読んでいくソースコードのコミットは2d9b9fb5b5f6015e66d3ad5cb96bc1ba117fd626
だ。
TL;DR
bin/rails s
がユーザーによって実行される。
コマンドの実行
まずbin/railsを見る。bin/railsはrails new
を実行したときに生成されるのだが、このひな形はrailties/lib/rails/generators/rails/app/templates/bin/railsにある。
# bin/rails #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands'
config/boot.rbとrails/commands.rbを見る。
# config/boot.rb ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' # Set up gems listed in the Gemfile.
- config/boot.rbはGemfileにあるgemをrequireするようだ。
# railties/lib/rails/commands.rb ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command)
rails s
と実行するとaliases
の中から"server"
という文字列を取得してrails server
を実行することになる。
rails/commands/commands_tasksを見る。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def initialize(argv) @argv = argv end def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else write_error_message(command) end end # ... end end
#parse_command
は--version
や--help
をそれぞれ"version"
と"help"
というコマンドに変換するもの。それ以外はそのまま返す。COMMAND_WHITELIST
に含まれていれば実行、そうでなければエラーを出力する。- 今回は
"server"
がcommand
に入るのでsend("server")
が実行され、#server
が実行されることになる。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def server set_application_directory! require_command!("server") Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end end # ... end end
#set_application_directory!
はconfig.ruがないディレクトリからでもrails s
を実行できるようにするためのものらしい。APP_PATH
bin/railsの中で代入されたconfig/application.rbなので、require "config/application"
をserver.start
の前に実行している。
設定の読み込み
# config/application.rb require File.expand_path('../boot', __FILE__) require 'rails/all' Bundler.require(*Rails.groups) module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
- 前述の通り、config/boot.rbは
require "bundler/setup"
を実行しておりGemfile.lockに記載されたバージョンのrubygemをrequire
している。
rails/all.rbを見る。
# railties/lib/rails/all.rb require "rails" %w( active_record action_controller action_view action_mailer active_job rails/test_unit sprockets ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end
- Railsを構成する各rubygemのrailtieを
require
している。
rails.rbを見る。
# railsties/lib/rails.rb module Rails # ... class << self # ... end end
- ここには
Rails.application
,Rails.configuration
,Rails.env
などの重要なメソッドが定義されているため、登場次第また見ていくことにする。
rails/all.rbとrails.rbについて見たので、config/applicationに戻る。
# config/application.rb # ... Bundler.require(*Rails.groups) module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
Rails.groups
は上述したrails.rbで定義されているのでさっそく見る。
# railties/lib/rails.rb module Rails class << self # ... def env @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") end # ... def groups(*groups) hash = groups.extract_options! env = Rails.env groups.unshift(:default, env) groups.concat ENV["RAILS_GROUPS"].to_s.split(",") groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) } groups.compact! groups.uniq! groups end # ... end end
Rails.groups
はRails.env
の値に合わせてBundlerが読みこむべきgroupを返す。Rails.env
は環境変数"RAILS_ENV"
または"RACK_ENV"
から実行環境を返す。
config/application.rbに戻る。
# config/application.rb # ... module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
SampleApp::Application
がRails::Application
を継承するとき、以下のような実装によってRails::Application.inherited
が呼ばれ、Rails.app_class
がSampleApp::Application
となる。
# railties/lib/rails/application.rb module Rails class Application < Engine class << self def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) end end end end
サーバーの起動
サーバー起動前にどういった設定を読み込んでいるか見たので、サーバーの起動について詳細に見ていく。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def server set_application_directory! require_command!("server") Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end end # ... end end
#require_command!("server")
でrequire "rails/commands/server"
をしている。
Rails::Server
はrails/commands/server.rbで定義されているので見る。
# railsties/lib/rails/commands/server module Rails class Server < ::Rack::Server # ... def initialize(*) super set_environment end # ... def start print_boot_information trap(:INT) { exit } create_tmp_directories log_to_stdout if options[:log_stdout] super ensure puts 'Exiting' unless @options && options[:daemonize] end # ... end end
- スーパークラスの
::Rack::Server
がサーバー起動において主な役割を果たしているようだ。 - これ以降はRackのソースコードを追うことになるが本題から反れるので、結論だけ言うとconfig.ruが実行されることになる。
config.ruを見る。
# config.ru require ::File.expand_path('../config/environment', __FILE__) run Rails.application
- config/environment.rbを読み込んでいる。
- その後
Rails.application
をrackアプリケーションとして実行している。
とりあえずconfig/environment.rbを見る。
# config/environment.rb require File.expand_path('../application', __FILE__) Rails.application.initialize!
- config/application.rbは既に読み込まれているはず。
Rails.application.initialize!
について見ていくため、まずはRails.application
の定義を見る。
# railties/lib/rails.rb module Rails class << self def application @application ||= (app_class.instance if app_class) end end end
app_class
は、config/application.rbでRails::Application
のサブクラスが定義されたときにそのサブクラスが代入される。SampleApp::Application.instance
が呼ばれているが、これのメソッドはRails::Application
に定義されていると思われる。
Rails::Application
を見る。
# railties/lib/rails/application.rb module Rails class Application < Engine class << self def instance super.run_load_hooks! end end def run_load_hooks! return self if @ran_load_hooks @ran_load_hooks = true ActiveSupport.run_load_hooks(:before_configuration, self) @initial_variable_values.each do |variable_name, value| if INITIAL_VARIABLES.include?(variable_name) instance_variable_set("@#{variable_name}", value) end end instance_eval(&@block) if @block self end end end
SampleApp::Application.instance
内でsuper.run_load_hooks!
が呼ばれている。このsuper
はスーパークラスで定義された.instance
を呼んでおり、スーパークラスをたどるとRails::Railtie.instance
が呼ばれていることがわかる。これはそのままnew
を呼んでインスタンスを返すだけだ。なので、super.run_load_hooks!
というのはSampleApp::Application#run_load_hooks!
を指す。SampleApp::Application#run_load_hooks!
はRails::Application
で上のように定義されており自分自身を返す。中でActiveSupport.run_load_hooks(:before_configuration, self)
を呼んでおり、これによって:before_configuration
をフックとして登録しておいた処理が実行される。- 結局、
Rails.application
はSampleApp::Application
インスタンスということになる。
initializerの実行
config/environment.rbに戻る。
# config/environment.rb require File.expand_path('../application', __FILE__) Rails.application.initialize!
Rails.application.initialize!
はつまりSampleApp::Application#initialize!
ということ。
Rails::Application
を見る。
# railties/lib/rails/application.rb module Rails class Application def initialize!(group=:default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end end end
#run_initializers
はRails::Initializable
で以下のように定義されている。
# railties/lib/rails/initializable.rb module Rails module Initializable def run_initializers(group=:default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end end end
#initializers
はRails::Application
で以下のように定義されている。
# railties/lib/rails/application.rb module Rails class Application def initializers Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) end end end
Bootstrap.initializers_for
とかFinisher.initializers_for
はRails::Initializable
モジュールで以下のように定義されている。
# railties/lib/rails/initializable.rb def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
Initializable::Collection
を初期化しているようだ。
# railties/lib/rails/initializable.rb class Collection < Array include TSort alias :tsort_each_node :each def tsort_each_child(initializer, &block) select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block) end def +(other) Collection.new(to_a + other.to_a) end end
TSort
モジュールはRubyの標準モジュールで、依存関係を解決する順番に並び替える(=トポロジカルソート)実装を提供する。#tsort_each_node
と#tsort_each_child
の2つを実装する必要がある。#tsort_each_node
はすべての要素を走査するメソッド、#tsort_each_child
は子要素を走査するメソッド。Collection#tsort_each_child
では与えられたinitializer
の前後のinitlaizer
に対してブロックを実行する。
Initializable.initializers_for
に戻る。
# railties/lib/rails/initializable.rb def initializers_chain initializers = Collection.new ancestors.reverse_each do |klass| next unless klass.respond_to?(:initializers) initializers = initializers + klass.initializers end initializers end def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
- 続いて
.initializers_chain
を見る。 .ancestors
はスーパークラスやinclude
しているモジュールを直接の親から順に並べて配列で返す。.ancestors.reverse_each
なので、最も遠いクラスまたはモジュールから順にinitializers
を取得して一つのCollection
に連結させている。
initializers_chain
の要素はおそらくInitializer
インスタンスだと思われるので、Initializer#bind
を見る。
# railties/lib/rails/Initializable.rb class Initializer def bind(context) return self if @context Initializer.new(@name, context, @options, &block) end end
Initializer#initialize
の第2引数は、Initializer#run
でinstance_exec
のレシーバとして利用される。今回の場合、これはRails::Application
インスタンスとなる。
Rails::Application#initializers
に戻る。
# railties/lib/rails/application.rb module Rails class Application def initializers Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) end end end
#railties_initializers
がまだ残っているので見る。
# railties/lib/rails/application.rb module Rails class Application def ordered_railties @ordered_railties ||= begin order = config.railties_order.map do |railtie| # ... end all = (railties - order) all.push(self) unless (all + order).include?(self) order.push(:all) unless order.include?(:all) index = order.index(:all) order[index] = all order end end def railties_initializers(current) initializers = [] ordered_railties.reverse.flatten.each do |r| if r == self initializers += current else initializers += r.initializers end end initializers end end end
config.railties_order
はデフォルトでは[:all]
を返すようになっているが、ここを変更することで実行するRailtieのinitializer
の順番を変更することもできるようだ。#ordered_railties
が返すのはall = (railties - order)
の部分なので、あとで詳しく#railties
について見る。- ある順番でソートされた各Railtieの
initializers
を結合して返している。
#railties
はRails::Engine
から継承されたメソッドなので見る。
# railties/lib/rails/engine.rb def railties @railties ||= Railties.new end
Rails::Engine::Railties
を見る。
# railties/lib/rails/engine/railties.rb def initialize @_all ||= ::Rails::Railtie.subclasses.map(&:instance) + ::Rails::Engine.subclasses.map(&:instance) end
::Rails::Railtie
またはRails::Engine
のサブクラスをすべて返している(!)
railties_initializers
に戻る。
# railties/lib/rails/application.rb def railties_initializers(current) initializers = [] ordered_railties.reverse.flatten.each do |r| if r == self initializers += current else initializers += r.initializers end end initializers end
ordered_railties
は::Rails::Railtie
または::Rails::Engine
のサブクラスすべてだということがわかった。- よって、それらの
initializers
をすべて連結したものを返していることになる。
Rails::Application#initializers
について見たので、#run_initializers
に戻る。
# railties/lib/rails/initializable.rb def run_initializers(group=:default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end
initializers
はInitializable::Collection
インスタンスなので、#tsort_each
によって依存関係を解決する順番で#each
を行う。
Initializer#run
を見る。
# railties/lib/rails/initializable.rb class Initializer def run(*args) @context.instance_exec(*args, &block) end end
@context
は#bind
によってセットされる。block
はInitializer
が初期化される際に渡されたブロックだ。このblock
は#bind
によってセットされた@context
をレシーバとして実行される。- 今回の場合、
@context
はRails::Application
インスタンスをレシーバとしてblock
が実行されることになる。
続いて、実行されるInitializer
がどのように初期化されて登録されているのか見ていく。これはRails::Initializable::ClassMethods.initializer
によって行われる。
# railties/lib/rails/initializable.rb def initializer(name, opts = {}, &blk) raise ArgumentError, "A block must be passed when defining an initializer" unless blk opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] } initializers << Initializer.new(name, nil, opts, &blk) end
opts[:after]
はInitializer
インスタンス間の依存関係の解決に利用される。initializer
で特に指定しなければ既存のinitializers
の最後の要素がinitializer#after
になる。
Railsアプリケーションの起動
initializers
の実行について一通り眺めたのでこれでconfig/environment.rbを読んだことになる。config.ruに戻る。
# config.ru require ::File.expand_path('../config/environment', __FILE__) run Rails.application
- ようやく
run Rails.application
でアプリケーションを起動できる。 Rails.application
がSampleApp::Application
インスタンスであることは上述の通り。