Ruby on Rails4を読む

はじめに

結構時間が空いてそろそろ4.2出るんじゃね?って雰囲気ですが4.1.4読みます。今回は「rake db:migrate」されたときの処理を読みます。

なお、ページタイトルが「db_migrate」となっているのはPukiWikiの都合です。

Rakefile

まずはエントリーポイントのRakefileです。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
require File.expand_path('../config/application', __FILE__)
 
Rails.application.load_tasks

railtiesに進み、load_tasksはRails::ApplicationではなくスーパークラスのEngineに定義されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
-
|
-
|
|
|
!
# 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のスーパークラスです)

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
|
|
|
!
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になります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
|
|
!
 
-
-
|
|
|
!
!
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を見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
-
|
|
-
-
|
|
-
-
|
!
!
!
!
|
|
!
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に移ります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
-
-
|
|
!
|
|
|
|
-
|
-
|
!
|
!
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

さて、というわけでdb:migrateが定義されている箇所がわかりました。Migratorクラスはactive_record/migration.rbに定義されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
|
|
|
|
|
|
|
|
|
!
 
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に移ります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
-
|
|
|
|
!
 
-
|
|
|
|
-
|
|
|
|
|
|
|
!
|
|
!
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。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
-
-
|
!
|
-
|
|
-
|
|
|
|
!
!
!
 
-
|
-
|
|
-
!
|
!
!
 
-
-
|
|
!
!
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

MIgrationProxy、一部省略してます。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
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

個々のmigrationは以下のような感じです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
-
-
-
|
|
|
|
!
!
!
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
 
      t.timestamps
    end
  end
end

Migrationクラスのインスタンスメソッドのmigrate。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
-
|
|
|
|
|
!
 
 
-
-
|
!
!
 
 
 
 
 
 
 
-
|
-
-
|
|
|
!
|
|
!
|
|
!
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が使われています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
-
|
|
-
-
-
|
|
!
!
|
|
!
!
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

今回はRakefileを対象としているのでconnectionがどうなっているかの詳細は見ません。ただ、かなり複雑なことをしているので簡単にだけ追っかけます。

active_record/railties.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
-
|
-
-
|
-
|
-
|
|
!
!
|
|
|
!
!
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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
-
-
-
|
|
|
|
-
|
!
|
|
|
!
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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
-
-
-
-
-
|
|
|
|
|
-
|
|
|
|
|
!
|
|
|
!
|
|
|
-
-
-
-
-
|
!
|
|
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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
-
-
-
|
|
|
!
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が返ってきたりして少し追ってることと違う動きをしているのかもしれませんが大体こんな感じで接続設定を行い、接続をプーリングし、データベースにアクセスしてテーブルを作成しているようです。

おわりに

今回は「rake db:migrate」の動きを見てきました。ポイントとしては以下の3つになります。

これでデータベースが作成されサーバが起動できるようになりました。次回は、「rails server」としたときのサーバ起動処理について見ていきたいと思います。


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS