はじめに
前回の記事で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つ目のmigration
はMigrationProxy
クラスのインスタンス、2つ目のmigration
はMigrationProxy
クラスの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
が実行されます。
参考