LoginSignup
3
2

More than 3 years have passed since last update.

Rails5 で ActionController::Parameters#fetch の返り値に []= を実行しても params の値を変更できない

Last updated at Posted at 2019-07-13

現象

Rails5 でActionController::Parameters#fetch の返り値に ActionController::Parameters#[]= を使用しても Hash の値を変更できないという現象です。

サンプルコード(Rails5の場合)
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
# => "original"

Rails4 では ActionController::Parameters#fetch の返り値に ActionController::Paramers#[]= を使用すると Hash の値を変更できていました。

サンプルコード(Rails4の場合)
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
# => "modified"

開発環境

Rails5: v5.0.7.2
Rails4: v4.2.11.1

解決できた方法

fetch ではなく [] で実装する方法

ActionController::Parameters#fetch を使わずに ActionController::Parameters#[] を使用すると、 params の値を変更することができました。

サンプルコード(Rails5)
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key][:hash_key] = "modified"

params[:param_key][:hash_key]
# => "modified"

背景

Rails4 から Rails5 にアップデートしたところ、 ActionController::Parametersfetch[]= を使って params の値を変更する箇所が、うまく動作しないことに気づきました。

サンプルコード
params.fetch(:schedule)[:start_at] = DateTime.parse("#{date} #{params.dig(:schedule, :start_at).in_time_zone('Tokyo')} JST")

Rails5 で ActionController::Parameters の実装が変わったことがきっかけではないかと考え、 Rails の内部実装を調査することにしました。

調査結果

Rails5 だと fetch の返り値に []= を実行しても参照元の値を変更できないことがある

  • fetch(key) で参照しようとした値が Hash だった場合、
    • HashActionController::Parameters のインスタンスに変換して返します
    • 返ってきたインスタンスに対して []= を実行しても参照元の値は変更されません
rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def fetch(key, *args)
  convert_value_to_parameters(
    @parameters.fetch(key) {
      if block_given?
        yield
      else
        args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
      end
    }
  )
end

Source on GitHub

ActionController::Parameters#fetch では ActiveSupport::HashWithIndifferentAccess クラスのインスタンスである @parameters に対して Hash#fetch メソッドが実行されます。

そして、その返り値を引数として ActionController::Parameters#convert_value_to_parameters が実行されます。

rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_value_to_parameters(value)
  case value
  when Array
    return value if converted_arrays.member?(value)
    converted = value.map { |_| convert_value_to_parameters(_) }
    converted_arrays << converted
    converted
  when Hash
    self.class.new(value)
  else
    value
  end
end

Source on GitHub

ActionController::Parameters#convert_value_to_parameters の引数に渡ってくる値のクラスが ActiveSupport::HashWithIndifferentAccess である場合、そのクラスは Hash を継承しているので、 self.class.new(value) が実行されます。

サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params.fetch(:param_key)[:hash_key] = "modified"

params.fetch(:param_key)[:hash_key]
=> "original"
params[:param_key][:hash_key]
=> "original"

上記の調査を元にサンプルコードを説明すると、 params.fetch(:param_key) の返り値は ActionController::Parameters.new({ hash_key: "original"}) になります。

上記で作成された ActionController::Parameters のインスタンスは params とは別のインスタンスなので、 [:hash_key] = "modified" を実行しても params の値は変更できません。

[] の返り値に []= を実行すれば参照元の値を変更できる

  • [](key) で参照しようとした値が Hash の場合、
    • HashActionController::Parameters のインスタンスに変換して返します
    • 変換した値を参照元の Hash に再代入します
    • 再代入されているため、返ってきたインスタンスに []= を実行すると参照元の値も変更できます
rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def [](key)
  convert_hashes_to_parameters(key, @parameters[key])
end

Source on GitHub

ActionController::Parameters#[] では ActionController::Parameters#convert_hashes_to_parameters が実行されます。

rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_hashes_to_parameters(key, value)
  converted = convert_value_to_parameters(value)
  @parameters[key] = converted unless converted.equal?(value)
  converted
end

Source on GitHub

[](key) で参照しようとした値が Hash だった場合、 その HashActionController::Parameters#convert_value_to_parameters によって、 ActionController::Parameters のインスタンスに変換されます。

さらに変換されたインスタンスを参照元の値に再代入した後に、そのインスタンスを返します。参照元に再代入しているため、返ってきたインスタンスの値を変更すると参照元の値も変更されます。

サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key][:hash_key] = "modified"

params[:param_key][:hash_key]
=> "modified"

上記の調査を元にサンプルコードを説明します。 params[:param_key] を実行することで、 params[:param_key] の値が ActionController::Parameters のインスタンスに変換され、さらに params[:param_key] に変換されたインスタンスが再代入されます。

ActionController::Parameters のインスタンスは params[:param_key] に再代入されているため、 params[:param_key] で返ってきたインスタンスに対して [:hash_key] = "modified" を実行すると、 params の値も変更されます。

備考

Hash ではなく ActionController::Parameters であれば fetch でも変更できる

例えば、以下のサンプルコードのように一度 ActionController::Parameters#[] を実行してしまえば、 params.fetch[:param_key][:hash_key] = "modified" でも params の値を変更することができます。

サンプルコード
hash = ActiveSupport::HashWithIndifferentAccess.new({ hash_key: "original" })
params = ActionController::Parameters.new({ param_key: hash })
params[:param_key] # ここで params[:param_key] に ActionController::Parameters に変換されたインスタンスが再代入される
params.fetch(:param_key)[:hash_key] = "modified"

params[:param_key][:hash_key]
=> "modified"

まず params[:param_key] が実行されることで、 params[:param_key]ActionController::Parameters のインスタンスが再代入されます。

つぎに params.fetch(:param_key) を実行すると、内部で convert_value_to_parameters が実行されるわけですが、 ここで引数に渡ってくる値は ActiveSupport::HashWithIndifferentAccess ではなく、 ActionController::Parameters のインスタンスになります。

rails/actionpack/lib/action_controller/metal/strong_parameters.rb
def convert_value_to_parameters(value)
  case value
  when Array
    return value if converted_arrays.member?(value)
    converted = value.map { |_| convert_value_to_parameters(_) }
    converted_arrays << converted
    converted
  when Hash
    self.class.new(value)
  else
    value
  end
end

よって、 params.fetch(:param_key) の返り値は params[:param_key] に再代入された ActionController::Parameters のインスタンスなので、 params.fetch(:param_key)[:hash_key] = "modified" を実行すれば、 params の値も変更されます。

3
2
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
3
2