4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

サイバーウェーブAdvent Calendar 2019

Day 25

マイグレーションファイルのdef upとdef self.upの違いについて調べてみた②

Last updated at Posted at 2019-12-26

はじめに

前回の記事nearest_delagateがなんなのか分からず、中途半端に終わってしまいました。
今回はその続きです。migrateタスクが定義されているところから追いかけてみようと思います。

ちなみに前回と同じくマイグレーションファイルが下記のようなものだとして話を進めます。

class CreateUsers < ActiveRecord::Migration[5.2]
  def up
    create_table :users do |t|
      t.string :name

      t.timestamps
    end
  end
end

環境

ruby 2.6.3
rails 5.2.3

active_record/railties/databases.rake


db_namespace = namespace :db do

  desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
  task migrate: :load_config do
    ActiveRecord::Tasks::DatabaseTasks.migrate
    db_namespace["_dump"].invoke
  end

コマンドライン上でrails db:migrateと入力してEnterを押すと、railsのモジュールが色々読み込まれたのちに、まず上記が実行されます。

ActiveRecord::Tasks::DatabaseTasks#migrate

module ActiveRecord
  module Tasks
    module DatabaseTasks

      def migrate
        check_target_version

        verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
        scope = ENV["SCOPE"]
        verbose_was, Migration.verbose = Migration.verbose, verbose
        Base.connection.migration_context.migrate(target_version) do |migration|
          scope.blank? || scope == migration.scope
        end
        ActiveRecord::Base.clear_cache!
      ensure
        Migration.v

Base.connection.migration_context.migrateがマイグレーションの中身っぽいです。migrateのレシーバを確認してみます。

> Base.connection.migration_context
=> #<ActiveRecord::MigrationContext:0x0000000005c3bcc0
 @migrations_paths=["/path/to/app/db/migrate"]>

MigrationContextがなんなのかちょっとわからなかったですが、マイグレーションファイルがあるパスをインスタンス変数に持っているようです。
MigrationContext#migrateを見にいきます。

ActiveRecord::MigrationContext#migrate

module ActiveRecord

  class MigrationContext

    def migrate(target_version = nil, &block)
      case
      when target_version.nil?
        up(target_version, &block)
      when current_version == 0 && target_version == 0
        []
      when current_version > target_version
        down(target_version, &block)
      else
        up(target_version, &block)
      end
    end

target_versionは、rails db:migrate VERSION=xxxxで指定できる値で、マイグレーションファイルの先頭にある数字10桁のことです。何もオプションをつけないで実行した場合、nilが返ります。

今回はnilの場合を見ていくので、MigrationContext#upが実行されます。

ActiveRecord::MigrationContext#up

def up(target_version = nil)
  selected_migrations = if block_given?
    migrations.select { |m| yield m }
  else
    migrations
  end

  Migrator.new(:up, selected_migrations, target_version).migrate
end

Migratorのインスタンスを作成しているのでMigrator#initializeを確認します。

module ActiveRecord

  class Migrator

    def initialize(direction, migrations, target_version = nil)
      @direction         = direction
      @target_version    = target_version
      @migrated_versions = nil
      @migrations        = migrations

      validate(@migrations)

      ActiveRecord::SchemaMigration.create_table
      ActiveRecord::InternalMetadata.create_table
    end

selected_migrationsは、例えば以下のようになります。

> selected_migrations
=> [#<struct ActiveRecord::MigrationProxy
  name="CreateUsers",
  version=20191020160206,
  filename="/home/ec2-user/environment/sample/db/migrate/20191020160206_create_users.rb",
  scope="">]

マイグレーションファイルに記述したクラス名、ファイル名の先頭の数字、ファイルパスを保持した、MigrationProxyというクラスのインスタンスの配列です。

上記を踏まえるとMigratorは以下のようになります。

Migrator.new(:up, selected_migrations, target_version)
=> #<ActiveRecord::Migrator:0x0000000004f87740
 @direction=:up,
 @migrated_versions=#<Set: {}>,
 @migrations=[#<struct ActiveRecord::MigrationProxy name="CreateUsers", version=20191020160206, filename="/home/ec2-user/environment/sample/db/migrate/20191020160206_create_users.rb", scope="">],
 @target_version=nil>

Migrator#migrateを見にいきます。

ActiveRecord::Migrator#migrate

def migrate
  if use_advisory_lock?
    with_advisory_lock { migrate_without_lock }
  else
    migrate_without_lock
  end
end

Migrator#migrate_without_lockを見にいきます。

ActiveRecord::Migrator#migrate_without_lock

def migrate_without_lock
  if invalid_target?
    raise UnknownMigrationVersionError.new(@target_version)
  end

  result = runnable.each do |migration|
    execute_migration_in_transaction(migration, @direction)
  end

  record_environment
  result
end

Migrator#runnableで、全マイグレーションの中から指定したバージョンまでのマイグレーションに限定しています。バージョン指定がない場合は全てのマイグレーションが入ります。尚、中身はMigrationProxyクラスのインスタンスです。

def runnable
  runnable = migrations[start..finish]
  if up?
    runnable.reject { |m| ran?(m) }
  else
    ...
  end
end

def start    # migrateのときは0を返します。
  up? ? 0 : (migrations.index(current) || 0)
end

def finish   # バージョン指定がある場合は、そのバージョンのインデックスを返します。
  migrations.index(target) || migrations.size - 1
end

def target
  migrations.detect { |m| m.version == @target_version }
end

Migrator#execute_migration_in_transactionを見にいきます。

ActiveRecord::Migrator#execute_migration_in_transaction

def execute_migration_in_transaction(migration, direction)
  return if down? && !migrated.include?(migration.version.to_i)
  return if up?   &&  migrated.include?(migration.version.to_i)

  Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger

  ddl_transaction(migration) do
->  migration.migrate(direction)
    record_version_state_after_migrating(migration.version)
  end
rescue => e
  msg = "An error has occurred, ".dup
  msg << "this and " if use_transaction?(migration)
  msg << "all later migrations canceled:\n\n#{e}"
  raise StandardError, msg, e.backtrace
end

上記で示した矢印のmigrationには、前で確認したようにMigartionProxyクラスのインスタンスが格納されています。

> migration
=> #<struct ActiveRecord::MigrationProxy
  name="CreateUsers",
  version=20191020160206,
  filename="/home/ec2-user/environment/sample/db/migrate/20191020160206_create_users.rb",
  scope="">

本来ならば、migration.migrateで、MigartionProxyクラスのインスタンスメソッドであるmigrateが実行されるはずですが、delegate(下記参照)により、privateメソッドのmigrationを実行した後、migrateを実行します。

MigrationProxy = Struct.new(:name, :version, :filename, :scope) do

  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

本来ならばmigration.migrateとなるところが、delegate :migrate, to: :migrationという記述があることによって、migration.migration.migrateとなります(1つ目のmigrationMigrationProxyクラスのインスタンス、2つ目のmigrationMigrationProxyクラスのprivateメソッドです)。
delegateに関して、詳しくはこちらなどを参考にしてください。)

このload_migration内のname.constantize.newで、マイグレーションファイルに記述したクラスのインスタンスが作成されます。

> name
=> "CreateUser"

> name.constantize.new(name, version)
=> #<CreateUsers:0x00000000050de260 @connection=nil, @name="CreateUsers", @version=20191020160206>

このインスタンスがmigrateメソッドを実行します。

ただ、マイグレーションファイルにmigrateメソッドは記述していないので、以下のような継承によりActiveRecord::Migration#migrateが実行されます。

class CreateUsers < ActiveRecord::Migration[5.2]

ActiveRecord::Migration#migrate

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

exec_migrationをみにいきます。

ActiveRecord::Migration#exec_migration

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

今、このメソッドのレシーバは、マイグレーションファイルに記述したCreateUsersのインスタンスです。
ファイル内でchangeを定義していればrespond_to?(:change)がtrueになりchangeが実行され、そうでなければsend(direction)が実行されます。

今回はchangeではなく、upもしくはself.upなので、

upの場合 → マイグレーションファイル内のupが実行される
self.upの場合 → ActiveRecord::Migration#upが実行される

といった感じです。


self.upの場合を見ていきます。

ActiveRecord::Migration#up

def up
  self.class.delegate = self
  return unless self.class.respond_to?(:up)
  self.class.up
end

マイグレーションファイルにself.upと定義しているので、self.class.respond_to?(:up)がtrueになります。
そしてself.class.up、つまり、マイグレーションファイルに記述したself.upが実行されます。

この後は前回の記事に書いたように、self.upならクラスメソッドのmethod_missingが、upならインスタンスメソッドのmethod_missingが実行されます。

参考

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?