[[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」としたときのサーバ起動処理について見ていきたいと思います。

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS