読者です 読者をやめる 読者になる 読者になる

RailsのMigrationのソースを追ってみたメモ

db:migrateのエントリポイント

おそらくこれ?

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/railties/databases.rake#L46

  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
  end

これがなぜ Rake に含まれるかはなぞとしてActiveRecord::Migratorが大事っぽ
verboseはまぁきっとそののままだろう。

ActiveRecord::Migrator

active_record/migration.rb にあるっぽ。 file名 == Class名じゃないと探しにくい

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/migration.rb#L565

ActiveRecord::Migrator.migrations_paths

とりあえず呼ばれているこれ調べてみる。

def migrations_paths
  @migrations_paths ||= ['db/migrate']
  # just to not break things if someone uses: migration_path = some_string
  Array(@migrations_paths)
end

名前と処理からして、Rails.root からの migrate ファイルを置き場所を指定してるっぽ。
・・場所かえれるのか!
Array()は "a" でも ["a"] でも ["a"] にするため。配列前提なのか。複数いける?

ActiveRecord::Migrator.migrate

いかにも本体っぽい名前

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

target_versionはENV["VERSION"]( rake db:migrate VERSION= )を渡した処理のことだろう。飛ばす。
となると up か

ActiveRecord::Migrator.up

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
  1. migrationを指定したパスからとってきて
  2. blockで渡されたロジックで選定して
  3. 実行

っていう風に読めるな。とってくるところから

ActiveRecord::Migrator.migrations

def migrations(paths)
  paths = Array(paths)

  # migrations_path下にある migration file っぽい名前のfile("9999_abc999.rb"みたいな)をとってきて
  files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]

  migrations = files.map do |file|
    # version(先頭の数値部分) 
    # name(その下 "." まで) 
    # scope(その下の "." まで。。なにこれ?)
    # をファイル名からとる
    version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first

    # versionがなければエラー(おそらく大雑把な不正なファイルチェック)
    raise IllegalMigrationNameError.new(file) unless version
    version = version.to_i
    name = name.camelize

    # proxyのインスタンスにする
    MigrationProxy.new(name, version, file, scope)
  end

  migrations.sort_by(&:version)
end

ActiveRecord::MigrationProxy

単純に値を保持しとくためのクラス?

# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
class MigrationProxy < Struct.new(:name, :version, :filename, :scope)

  def initialize(name, version, filename, scope)
    super
    @migration = nil
  end

  def basename
    File.basename(filename)
  end

  # 何このメソッド
  # => 調べた
  #   migrate, announce, :write メソッドを migration メソッドで
  #   取得できるinstanceに委譲する。この仕組みはまた今度調べよう。
  delegate :migrate, :announce, :write, :to => :migration

  private

    # migration本体のクラスをloadする
    def migration
      # nil ガード
      @migration ||= load_migration
    end

    def load_migration
      # migration fileをrequire
      require(File.expand_path(filename))
      
      # constantizeは "String" => String にしてくれる超便利Rails拡張メソッド
      # もはや狂気すら感じるくらいDRY
      name.constantize.new
    end

end

db:migrateのエントリポイント

またここにもどってきてblockをチェック
ENV["SCOPE"]を渡されていたらそのSCOPEに絞るってことかな

  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
  end

ActiveRecord::Migrator#initialzie

ほんでもって、今までの処理でとってきた migrations(<#MigrationProxy>), ENV["VERSION"]を渡して自身のインスタンスをnew

# 引数は(direction(:upか:down?), migrations, target_version(ENV["VERSION"])
self.new(:up, migrations, target_version).migrate
def initialize(direction, migrations, target_version = nil)
  raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?

  @direction         = direction
  @target_version    = target_version
  @migrated_versions = nil

  # migrationsの中からStringのmigrationがあるかチェックあればwarn
  if Array(migrations).grep(String).empty?
    @migrations = migrations
  else
    ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
    @migrations = self.class.migrations(migrations)
  end

  #なんかのチェック。。。とばす
  validate(@migrations)

  # migration情報を管理する scheme_migrationテーブルを無ければ作る。
  ActiveRecord::SchemaMigration.create_table
end

ActiveRecord::Migrator#migrate

def migrate
  # 今のコンテキスト(db:migrate)では @target_version が nilのはずだから無視
  if !target && @target_version && @target_version > 0
    raise UnknownMigrationVersionError.new(@target_version)
  end

  # 実行対象のmigrationにしぼる
  running = runnable

  # blockでさらにしぼれたみたいだけど deprecationになっているみたい
  if block_given?
    ActiveSupport::Deprecation.warn(<<-eomsg)
block argument to migrate is deprecated, please filter migrations before constructing the migrator
    eomsg
    running.select! { |m| yield m }
  end

  # 実行対象 migration(MigrationProxyのインスタンス)のループ
  running.each do |migration|
    Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger

    begin
      # DDLトランザクションがサポートされていればトランザクションを開始する
      ddl_transaction do
        migration.migrate(@direction)
        record_version_state_after_migrating(migration.version)
      end
    rescue => e
      canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
      raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
    end
  end
end

ActiveRecord::Migrator#runable

def runnable
  # upの場合は0からmigrations.length - 1まで(要するに全部)
  runnable = migrations[start..finish]
  if up?
    # ran? は排除。要するに実行済みは排除
    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

ActiveRecord::Migrator#run?

def ran?(migration)
  migrated.include?(migration.version.to_i)
end

ActiveRecord::Migrator#migrated

def migrated
  @migrated_versions ||= Set.new(self.class.get_all_versions)
end

ActiveRecord::Migrator.get_all_versions

def get_all_versions
  # shema_migrationテーブルの全てのレコード
  SchemaMigration.all.map { |x| x.version.to_i }.sort
end

ActiveRecord::Migrator#ddl_transaction

def ddl_transaction
  # DDLトランザクション(Create tableとかもrollbackできる。PostgreSQLはいけてMySQLは無理)がサポートされていれば
  if Base.connection.supports_ddl_transactions?
    # トランザクション内でmigrationする
    Base.transaction { yield }
  else
    yield
  end
end

ActiveRecord::Migration#migrate

MigrationProxy#migrateはproxyの中にある、migration fileに書いてあるClassをnewしたメソッドに委譲されているはずなので、db/migrate 下のファイルをみてみると。
たいていこんな感じで書かれている。

class SomeModels < ActiveRecord::Migration
  def change
    add_column :some_models, :some_field, :string
  end
end

この中に migrate メソッドがないのでおそらく Super Classにあるはず
でこれ
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/migration.rb#L402

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|
    @connection = conn
    if respond_to?(:change)
      if direction == :down
        recorder = CommandRecorder.new(@connection)
        suppress_messages do
          @connection = recorder
          change
        end
        @connection = conn
        time = Benchmark.measure {
          self.revert {
            recorder.inverse.each do |cmd, args|
              send(cmd, *args)
            end
          }
        }
      else
        # change メソッドを呼ぶ。migrationの中身だね
        time = Benchmark.measure { change }
      end
    else
      time = Benchmark.measure { send(direction) }
    end
    @connection = nil
  end

  case direction
  when :up   then announce "migrated (%.4fs)" % time.real; write
  when :down then announce "reverted (%.4fs)" % time.real; write
  end
end

ActiveRecord::Migration#method_missing

ほんで、add_columnなんてメソッドがないので当然 method_missingにきて、connectionでとれるインスタンスに委譲されているみたい。

def method_missing(method, *arguments, &block)
  arg_list = arguments.map{ |a| a.inspect } * ', '

  say_with_time "#{method}(#{arg_list})" do
    unless reverting?
      unless arguments.empty? || method == :execute
        arguments[0] = Migrator.proper_table_name(arguments.first)
        arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
      end
    end
    return super unless connection.respond_to?(method)
    connection.send(method, *arguments, &block)
  end
end

ActiveRecord:: ConnectionAdapters:: SchemaStatements#add_column

このクラスの中に migrations ファイル記述する際に使うメソッドが用意されている。
SQLが見れたので今日はここまで。
途中から息切れ。。。

def add_column(table_name, column_name, type, options = {})
  add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
  add_column_options!(add_column_sql, options)
  execute(add_column_sql)
end