バージョン指定をしない時、Railsのrake db:migrateでchangeメソッドを実行するMigrationのインスタンスの配列をどうやって取得しているのか
migrationで厄介な対応が必要な場合、SchemaMigration
をいじって整合性を取る必要性がある、と先輩が言っていたのを聞いて、ふむふむと追いかけてみました。
なお、この記事に出てくるコードのRailsのバージョンは、5.0.0.alpha
です。
まとめ
バージョン指定をしない時、Railsのrake db:migrateでchangeメソッドを実行するMigrationのインスタンスの配列の取得手順について.
- migrationファイルが入ったパスでmigrationファイルを探し、それに対応するMigrationクラスのサブクラスのインスタンスの配列を作る
- 上記を
prefix
によるバージョンで並び替える- upの場合、
ActiveRecord::SchemaMigration
に対応したテーブルに入っているレコード数分は飛ばした上記の配列のサブセットを取得 - downの場合、上記の配列の0番目から指定したバージョンまでのサブセットを取得
- upの場合、
- さらに、上のように得た配列の中身に対して、
- upの場合、もう一度
SchemaMigration
のテーブルに入っている値から、その中にすでにmigrationされているものがないか調べて除外 - downの場合、指定したバージョンを除外した後、
SchemaMigration
のテーブルに入っている値だけを選別している
- upの場合、もう一度
Migrationをする範囲を指定しているらしきメソッドに行き着くまで
rake db:migrate
コマンドで実行されている処理を探したいのですが、rake ...
で動くということは、libsとかtasksとかに置いてありそうだな、ということで探すと、activte_record/tasks/database_tasks.rb
にそれらしいのがありました。
module ActiveRecord module Tasks # :nodoc: ... module DatabaseTasks extend self ... def migrate verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil scope = ENV['SCOPE'] verbose_was, Migration.verbose = Migration.verbose, verbose Migrator.migrate(Migrator.migrations_paths, version) do |migration| scope.blank? || scope == migration.scope end ensure Migration.verbose = verbose_was end
これを読んでいけばいいのでしょうか。
と思ったら、意外に変数を受け渡す感じのメソッドが続きます。
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
def up(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? new(:up, migrations, target_version).migrate end
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 = migrations validate(@migrations) Base.connection.initialize_schema_migrations_table end
Migrationをする範囲を指定しているらしきメソッド
バージョンを指定しなかった場合の挙動に関して知りたい場合、Migration#migrate
メソッドの中身以降を読んでいけばいいのかな、という感じでした。
def migrate # def target # migrations.detect { |m| m.version == @target_version } # end # なので、@target_versionがある(=バージョン指定をした)場合は、 # 指定のバージョンがなかったら、例外を発生して終わる 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
Migrationをする範囲を指定しているらしきメソッドの中身1
まず、注目したいのは、Migration#runnable
メソッドです。
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
このメソッドの返り値で実行するmigrationが決まっています。
runnable = migrations[start..finish]
の行をもうちょっとじっくり眺めてみます。
startの部分
downの場合は0で、upの場合はActiveRecord::SchemaMigration
のテーブルにはいっているレコードの数か0になっています。
def start up? ? 0 : (migrations.index(current) || 0) end # 関連コード def current_migration migrations.detect { |m| m.version == current_version } end alias :current :current_migration def current_version migrated.max || 0 end def migrated @migrated_versions ||= Set.new(self.class.get_all_versions) end
get_all_versions
の部分ですが、ここがActiveRecord::SchemaMigration
モデルに対応したテーブルを見に行っています。
def get_all_versions(connection = Base.connection) if connection.table_exists?(schema_migrations_table_name) SchemaMigration.all.map { |x| x.version.to_i }.sort else [] end end
finishの部分
バージョン指定がなければ、migrationsの配列の最後のindexを返します
def finish migrations.index(target) || migrations.size - 1 end def target migrations.detect { |m| m.version == @target_version } end
migrationsの部分
migrationファイルの入ったパスを探しに行って、ファイルから読み込んで作ったActiveRecord::Migrationのサブクラスの配列を作っているっぽいです。
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
以上をまとめると、runnable = migrations[start..finish]
は何も指定がない場合、
- migrationファイルが入ったパスでmigrationファイルを探し、それに対応するMigrationクラスのサブクラスのインスタンスの配列を作る
- 上記を
prefix
によるバージョンで並び替える- upの場合、
ActiveRecord::SchemaMigration
に対応したテーブルに入っているレコード数分は飛ばした上記の配列のサブセットを取得 - downの場合、上記の配列の0番目から指定したバージョンまでのサブセットを取得
- upの場合、
しているようです。
Migrationをする範囲を指定しているらしきメソッドの中身2
そうして取得したMigrationのサブクラスのインスタンスについてどうしているかというと、
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
def ran?(migration) migrated.include?(migration.version.to_i) end
- upの場合、もう一度
SchemaMigration
のテーブルに入っている値から、その中にすでにmigrationされているものがないか調べて除外 - downの場合、指定したバージョンを除外した後、
SchemaMigration
のテーブルに入っている値だけを選別している
こういうコードになってたんですね。