[[Ruby on Rails4を読む]] #contents *はじめに [#yd126b3e] 結構時間が空いてそろそろ4.2出るんじゃね?って雰囲気ですが4.1.4読みます。今回は「rake db:migrate」されたときの処理を読みます。 なお、ページタイトルが「db_migrate」となっているのはPukiWikiの都合です。 *Rakefile [#le058bac] まずはエントリーポイントのRakefileです。 #code(Ruby){{ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks }} railtiesに進み、load_tasksはRails::ApplicationではなくスーパークラスのEngineに定義されています。 #code(Ruby){{ # Load Rake, railties tasks and invoke the registered hooks. # Check <tt>Rails::Railtie.rake_tasks</tt> for more info. def load_tasks(app=self) require "rake" run_tasks_blocks(app) self end }} コメントに書いてあるRailtie.rake_tasksを見てみます。(RailtieはEngineのスーパークラスです) #code(Ruby){{ def rake_tasks(&blk) @rake_tasks ||= [] @rake_tasks << blk if blk @rake_tasks end }} 何の変哲もないように見えますが、@rake_tasksを返しているのが実はミソです。こうすることで、 rake_tasks do ... end と書くことによりタスクの定義が、 rake_tasks と書くことにより定義されたタスクの取得が可能です。というか初めこれに気づかず以降で書く箇所がどう動いているのかわかりませんでした:-< 今回対象としているdb:migrate(active_record)のrake_tasksは後で見るとして、run_tasks_blocksを見てみましょう。run_tasks_blocksはApplicaton、Engine、Railtie全部で定義されていますが今回重要なのはRailtieになります。 #code(Ruby){{ def run_tasks_blocks(app) #:nodoc: extend Rake::DSL each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) } end def each_registered_block(type, &block) klass = self.class while klass.respond_to?(type) klass.public_send(type).each(&block) klass = klass.superclass end end }} ここで先ほど書いたrake_tasksメソッドの挙動が利用されています。ではactive_recordのrailties.rbを見てみましょう。 #code(Ruby){{ rake_tasks do require "active_record/base" namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) if engine.paths['db/migrate'].existent ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a end end end end load "active_record/railties/databases.rake" end }} ifの中身が実行されるかとENGINE_PATHが定義されている場所を探してみましたがどうやら通常は定義されていないようです。database.rakeに移ります。 #code(Ruby){{ db_namespace = namespace :db do task :load_config do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end (中略) desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) end db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end }} こちらでもload_configが定義されています。同じタスクが複数登録された場合は定義された順に実行されるようです。 *ActiveRecord::Migrator [#gb5deebc] さて、というわけでdb:migrateが定義されている箇所がわかりました。Migratorクラスはactive_record/migration.rbに定義されています。 #code(Ruby){{ def migrate(migrations_paths, target_version = nil, &block) case when target_version.nil? up(migrations_paths, target_version, &block) when current_version == 0 && target_version == 0 [] when current_version > target_version down(migrations_paths, target_version, &block) else up(migrations_paths, target_version, &block) end end }} バージョンは指定してないのでupに移ります。 #code(Ruby){{ def up(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? self.new(:up, migrations, target_version).migrate end def migrations(paths) paths = Array(paths) files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize MigrationProxy.new(name, version, file, scope) end migrations.sort_by(&:version) end }} 念のため、migrations_pathsに入っているのは["<アプリのディレクトリ>/db/migrate"]です。 続いてインスタンスメソッドの方のmigrate。 #code(Ruby){{ def migrate if !target && @target_version && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin execute_migration_in_transaction(migration, @direction) rescue => e canceled_msg = use_transaction?(migration) ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end end def runnable runnable = migrations[start..finish] if up? runnable.reject { |m| ran?(m) } else # skip the last migration if we're headed down, but not ALL the way down runnable.pop if target runnable.find_all { |m| ran?(m) } end end def execute_migration_in_transaction(migration, direction) ddl_transaction(migration) do migration.migrate(direction) record_version_state_after_migrating(migration.version) end end }} start, finish, ran?はまあ想像がつくだろうと思うので省きます。ともかくこれで個々のmigrationまで進みました。 *MigrationProxy [#m927281c] MIgrationProxy、一部省略してます。 #code(Ruby)){{ class MigrationProxy < Struct.new(:name, :version, :filename, :scope) def initialize(name, version, filename, scope) super @migration = nil end delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private def migration @migration ||= load_migration end def load_migration require(File.expand_path(filename)) name.constantize.new(name, version) end end }} migrateが呼ばれると、migrationが参照され、load_migrationが呼ばれ、個々のmigrationがrequire、インスタンスを作成してそのmigrateメソッドが呼ばれます。 *Migration [#c064191f] 個々のmigrationは以下のような感じです。 #code(Ruby){{ class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.string :email t.timestamps end end end }} Migrationクラスのインスタンスメソッドのmigrate。 #code(Ruby){{ def migrate(direction) return unless respond_to?(direction) case direction when :up then announce "migrating" when :down then announce "reverting" end time = nil ActiveRecord::Base.connection_pool.with_connection do |conn| time = Benchmark.measure do exec_migration(conn, direction) end end case direction when :up then announce "migrated (%.4fs)" % time.real; write when :down then announce "reverted (%.4fs)" % time.real; write end end def exec_migration(conn, direction) @connection = conn if respond_to?(:change) if direction == :down revert { change } else change end else send(direction) end ensure @connection = nil end }} 個々のmigrationのchangeが呼ばれました。 さて、Migrationを見てもcreate_tableメソッドは見当たりません。ここでも伝家の宝刀method_missingが使われています。 #code(Ruby){{ def method_missing(method, *arguments, &block) arg_list = arguments.map{ |a| a.inspect } * ', ' say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table end end return super unless connection.respond_to?(method) connection.send(method, *arguments, &block) end end }} *connection [#t72a65a4] 今回はRakefileを対象としているのでconnectionがどうなっているかの詳細は見ません。ただ、かなり複雑なことをしているので簡単にだけ追っかけます。 active_record/railties.rb #code(Ruby){{ module ActiveRecord class Railtie < Rails::Railtie # :nodoc: # This sets the database configuration from Configuration#database_configuration # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do class ActiveRecord::NoDatabaseError remove_possible_method :extend_message def extend_message(message) message << "Run `$ bin/rake db:create db:migrate` to create your database" message end end self.configurations = Rails.application.config.database_configuration establish_connection end end }} いまいちどのタイミングで接続が確立されているかわからなかったのですが、答えは、「ロードされたとき」のようです。 connection_handling.rb #code(Ruby){{ module ActiveRecord module ConnectionHandling def establish_connection(spec = nil) spec ||= DEFAULT_ENV.call.to_sym resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations spec = resolver.spec(spec) unless respond_to?(spec.adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" end remove_connection connection_handler.establish_connection self, spec end }} conneection_specification.rb connection_specification.rb #code(Ruby){{ module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: class Resolver # :nodoc: def spec(config) spec = resolve(config).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter" begin require path_to_adapter rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" ConnectionSpecification.new(spec, adapter_method) end } core.rb #code(Ruby){{ module ActiveRecord module Core included do def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new }} connection_pool.rb #code(Ruby){{ module ActiveRecord module ConnectionAdapters class ConnectionHandler def establish_connection(owner, spec) @class_to_pool.clear raise RuntimeError, "Anonymous class is not allowed." unless owner.name owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) end }} 実はpメソッドでいろいろ調べてみるとnilが返ってきたりして少し追ってることと違う動きをしているのかもしれませんが大体こんな感じで接続設定を行い、接続をプーリングし、データベースにアクセスしてテーブルを作成しているようです。 *おわりに [#a3d2ca1c] 今回は「rake db:migrate」の動きを見てきました。ポイントとしては以下の3つになります。 -Rakefileがどのように読み込まれていくか -migration処理。クラスメソッド、インスタンスメソッド、いろんなクラスにmigrateがあるので勘違いのないように注意 -個々のmigrationの処理。詳しくは見なかったけどデータベースとのやり取り これでデータベースが作成されサーバが起動できるようになりました。次回は、「rails server」としたときのサーバ起動処理について見ていきたいと思います。