2018-02-27

ActiveRecord::PendingMigrationErrorはどうやって発生するのか

開発中のエラーでよく出てくるActiveRecord::PendingMigrationErrorについて調べてみました。Railsのバージョンは5.1.5です。

開発環境のエラー画面でPendingMigrationErrorが発生するのは以下の設定がされていることが条件です。

config.active_record.migration_error = :page_load

ActiveRecord::RailtieのinitializerでRackミドルウェアにActiveRecord::Migration::CheckPendingを設定します。

module ActiveRecord
  class Railtie < Rails::Railtie # :nodoc:
    initializer "active_record.migration_error" do
      if config.active_record.delete(:migration_error) == :page_load
        config.app_middleware.insert_after ::ActionDispatch::Callbacks,
          ActiveRecord::Migration::CheckPending
      end
    end

ActiveRecord::Migration::CheckPending#callはActiveRecord::Migration.check_pending!を呼び出します。

module ActiveRecord
  class Migration
    class CheckPending
      def initialize(app)
        @app = app
        @last_check = 0
      end

      def call(env)
        mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
        if @last_check < mtime
          ActiveRecord::Migration.check_pending!(connection)
          @last_check = mtime
        end
        @app.call(env)
      end

      private

        def connection
          ActiveRecord::Base.connection
        end
    end

check_pending!はActiveRecord::Migrator.needs_migration?を呼び出し、trueのときにActiveRecord::PendingMigrationErrorをraiseします。

    class << self
      def check_pending!(connection = Base.connection)
        raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
      end

needs_migration?ではdb/migratesディレクトリ内のマイグレーションファイル名から正規表現でバージョンを取得し、実際にマイグレーションしたバージョンが入るschema_migrationsのレコードと比較し、マイグレーションしていないファイルが存在すればtrueを返します。

    MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:

    class << self
      def needs_migration?(connection = Base.connection)
        (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
      end

      def parse_migration_filename(filename) # :nodoc:
        File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
      end

      def migrations(paths)
        paths = Array(paths)

        migrations = migration_files(paths).map do |file|
          version, name, scope = parse_migration_filename(file)
          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

get_all_versionsはActiveRecord::SchemaMigration.all_versionsを呼び出します。schema_migrations_table_nameはschema_migrationsに設定されているため、ActiveRecord::SchemaMigrationはテーブル名がschema_migrationsのActiveRecordとして振る舞います。

module ActiveRecord
  class SchemaMigration < ActiveRecord::Base # :nodoc:
    class << self
      def primary_key
        "version"
      end

      def table_name
        "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
      end
# ...
      def all_versions
        order(:version).pluck(:version)
      end

まとめると、Rackミドルウェアを使ってマイグレーションファイルとDB内のマイグレーション情報を比較してActiveRecord::PendingMigrationErrorを出しています。

このエントリーをはてなブックマークに追加