2018-03-01

ActiveRecord::NoEnvironmentInSchemaErrorについて

RailsでActiveRecord::NoEnvironmentInSchemaErrorが出たときの原因と対応方法の備忘録。Railsのバージョンは5.1.5です。

開発環境でDBを作り直す際、db:setupではなくdb:migrateを使ってマイグレーションをする場合、以下のコマンドでDBのセットアップを行っていました。

$ bundle exec rake db:drop db:create db:migrate

このとき、db:migrateでコケて、マイグレーションファイルを修正後に再度上記のコマンドを叩くと以下のように、ActiveRecord::NoEnvironmentInSchemaErrorが発生します。

rake aborted!
ActiveRecord::NoEnvironmentInSchemaError:

Environment data not found in the schema. To resolve this issue, run:

        bin/rails db:environment:set RAILS_ENV=development

対応方法としてはエラーに記載の通り、 db:environment:setを実行すれば良いのですが、なぜこのエラーが発生し、db:environment:setのRakeタスクで解消するのか?を説明していきます。

ActiveRecord::NoEnvironmentInSchemaError

まずはNoEnvironmentInSchemaErrorがどういったケースで発生するのかをコードベースで追っていきます。
db_namespace = namespace :db do
  task check_protected_environments: [:environment, :load_config] do
    ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
  end

  desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases."
  task drop: [:load_config, :check_protected_environments] do
    db_namespace["drop:_unsafe"].invoke
  end

dropのタスクではcheck_protected_environmentsが呼ばれます。このタスクはActiveRecord::Tasks::DatabaseTasks.check_protected_environments!を呼び出します。

module ActiveRecord
  module Tasks # :nodoc:
    module DatabaseTasks
      def check_protected_environments!
        unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"]
          current = ActiveRecord::Migrator.current_environment
          stored  = ActiveRecord::Migrator.last_stored_environment

          if ActiveRecord::Migrator.protected_environment?
            raise ActiveRecord::ProtectedEnvironmentError.new(stored)
          end

          if stored && stored != current
            raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
          end
        end
      rescue ActiveRecord::NoDatabaseError
      end

ActiveRecord::Migrator.last_stored_environmentでActiveRecord::InternalMetadata[:environment]を取得できなければNoEnvironmentInSchemaErrorが発生します。

module ActiveRecord
  class Migrator#:nodoc:
    class << self
      def self.last_stored_environment
        return nil if current_version == 0
        raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?

        environment = ActiveRecord::InternalMetadata[:environment]
        raise NoEnvironmentInSchemaError unless environment
        environment
      end

ActiveRecord::InternalMetadataはar_internal_metadataテーブルからRAILS_ENVの値に応じたレコードを取得・生成します。よって、NoEnvironmentInSchemaErrorが発生する=ar_internal_metadataのレコードが存在しない、ということになります。

module ActiveRecord
  # This class is used to create a table that keeps track of values and keys such
  # as which environment migrations were run in.
  class InternalMetadata < ActiveRecord::Base # :nodoc:
    class << self
      def primary_key
        "key"
      end

      def table_name
        "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
      end

      def []=(key, value)
        find_or_initialize_by(key: key).update_attributes!(value: value)
      end

      def [](key)
        where(key: key).pluck(:value).first
      end

一方、db:migrateのときはActiveRecord::Migrator#record_environment経由でActiveRecord::InternalMetadata[:environment]が設定されます。

module ActiveRecord
  class Migrator#:nodoc:
    def record_environment
      return if down?
      ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
    end

マイグレーションが終わったタイミングでar_internal_metadataのレコードが生成されるので、マイグレーションがエラーになるとレコードが生成されません。したがって、この状態でdb:dropを叩くとar_internal_metadataのレコードが見つからずNoEnvironmentInSchemaErrorが発生することになります。

ということで、解決方法もar_internal_metadataを生成すれば良いので、db:environment:setを叩けば良いことになります。

db_namespace = namespace :db do
  desc "Set the environment value for the database"
  task "environment:set" => [:environment, :load_config] do
    ActiveRecord::InternalMetadata.create_table
    ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
  end
このエントリーをはてなブックマークに追加