はじめに
Railsに関するトリビア的なネタです。
QiitaやZennを見ていると、ときどきRailsのコントローラに出てくるparamsをハッシュ(Hashオブジェクト)だと説明している記事を見かけます。
しかし、paramsはハッシュではありません。
確かめてみよう
こんな感じでparamsの中身をputsしてみましょう。
def show
puts params
end
うん、とてもハッシュっぽいですね。
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
では、こうするとどうでしょうか?
paramsがハッシュならどちらもtrueになるはずです。
def show
# paramsがハッシュそのものならtrue
puts params.instance_of?(Hash)
# paramsがHashクラスを継承していればtrue
puts params.is_a?(Hash)
end
結果はこうなりました。予想に反してどちらもfalseですね 🤔
false
false
paramsの正体は ActionController::Parameters
ではparamsは何者なんでしょうか?
クラス名を確認してみましょう。
def show
puts params.class
end
すると、以下のような出力になりました。
ActionController::Parameters
そうなんです、paramsはハッシュではなく ActionController::Parameters クラスのインスタンスなんです。
このクラスはHashクラスを継承したりしていないので、オブジェクト指向プログラミングでいうところのis-aの関係が成り立ちません。
念のため、 ActionController::Parameters クラスの継承関係を確認してみましょう。
def show
# ActionController::Parametersの継承関係を確認する
puts params.class.ancestors
end
ご覧のとおり、Hashがどこにも含まれていないことがわかります。
ActionController::Parameters
ActiveSupport::DeepMergeable
ActiveSupport::Dependencies::RequireDependency
ActiveSupport::ToJsonWithActiveSupportEncoder
Object
PP::ObjectMixin
ActiveSupport::Tryable
JSON::Ext::Generator::GeneratorMethods::Object
DEBUGGER__::TrapInterceptor
Kernel
BasicObject
paramsは変数でもない
さらにいうと、paramsは変数でもありません。
ふだん滅多に意識することはないかもしれませんが、paramsはメソッドです。
その証拠に、()
を付けたりself.
を付けて呼び出すことができます。
def show
# メソッドなので、()を付けたりself.を付けて呼び出せる
puts params()
puts self.params
end
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
{"id"=>"1", "controller"=>"projects", "action"=>"show"}
paramsは ActionController::Base クラスに(厳密にはそのクラスがincludeしている ActionController::StrongParameters モジュールに)定義されているメソッドです。
ためしに以下のようなコードを書いて確認してみましょう。
def show
# paramsメソッドの情報を確認する
puts method(:params)
end
実行結果を見るとparamsがコントローラ内でメソッドで定義されたメソッドであることがわかります。
#<Method: ProjectsController(ActionController::StrongParameters)#params() /Users/jnito/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/actionpack-7.1.2/lib/action_controller/metal/strong_parameters.rb:1281>
メソッドなので、paramsと[]の間にスペースが入るとエラーになる
もし、paramsがローカル変数だったら、paramsと[]
の間にスペースが入っても値を取得できます。
# paramsをローカル変数として定義
params = {id: 123}
# paramsがローカル変数ならスペースが挟まれてもOK(ふつうはしませんが)
params [:id]
#=> 123
しかし、Railsのparamsはメソッドなので、スペースが入るとエラーになります。
def show
# [ の手前にスペースが入るとエラー
params [:id]
end
ArgumentError: wrong number of arguments (given 1, expected 0)
これは[:id]
がparamsメソッドの引数と見なされるためです。
# params [:id] は以下のようなコードを書いたのと同じ
params([:id])
paramsメソッドは引数を受け取らないメソッドなので、引数の個数が合わず(given 1, expected 0)、エラーになります。
よってRailsのparamsは必ずスペースなしで[]
を呼び出さなければいけません。
def show
# [ の手前にスペースを入れてはいけない
params[:id]
end
コードを書くときに、わざわざparams [:id]
のようにスペースを空けて呼び出すことはまずありませんが、Rails初心者さんはたまにこういううっかりミスを犯してエラーを起こすことがあるので注意してください。
ハッシュじゃないのでシンボルでも文字列でも値を取得できる
すでに説明したとおり、Railsのparamsはハッシュではなく ActionController::Parameters クラスのインスタンスを返すメソッドです。
ハッシュではないので、 ActionController::Parameters クラスの[]
メソッドに指定するキーはシンボルでも文字列でも同じように動作します。
def show
# シンボルでキーを指定する
params[:id]
# 文字列でキーを指定してもOK
params['id']
end
これは ActionController::Parameters クラスが内部的にパラメータを ActiveSupport::HashWithIndifferentAccess オブジェクトとして保持しているためです(下から4行目の @parameters = parameters.with_indifferent_access
の部分に注目)。
# https://github.com/rails/rails/blob/v7.1.3/actionpack/lib/action_controller/metal/strong_parameters.rb#L268C69-L279C8
def initialize(parameters = {}, logging_context = {})
parameters.each_key do |key|
unless key.is_a?(String) || key.is_a?(Symbol)
raise InvalidParameterKey, "all keys must be Strings or Symbols, got: #{key.class}"
end
end
@parameters = parameters.with_indifferent_access
@logging_context = logging_context
@permitted = self.class.permit_all_parameters
end
ちなみに ActiveSupport::HashWithIndifferentAccess クラスはHashクラスを拡張してシンボルのキーと文字列のキーを同列に扱うようにしたRailsの(正確にはActiveSupportの)クラスです。
詳しくは以下のAPIドキュメントを参照してください。
参考:Sinatraのparamsも同様にシンボルでも文字列でも値を取得できる
Ruby製のwebフレームワークであるSinatraでもparams
はメソッドで、その値はシンボルでも文字列でも取得できるようになっています。
get '/' do
# params[:name]でもparams['name']でもOK
"Hello, #{params[:name]}!"
end
なお、Sinatraの場合はSinatra::IndifferentHashという独自のクラスを使って実装されているようです。
そもそもなんでparamsはハッシュじゃないの?
paramsが単純なハッシュではなく ActionController::Parameters オブジェクトを返す理由はなぜでしょうか?
それはマスアサインメント脆弱性を防止するためのStrong Parametersという仕組みを実現するためです。
ひとことで言えば、Railsが標準で用意しているセキュリティ対策ですね。
strong parametersは、Action ControllerのパラメータをActive Modelの「マスアサインメント」で利用することを禁止します(許可されたパラメータは除く)。したがって、開発者は、マスアップデートを許可する属性をコントローラで明示的に指定しなければなりません。strong parametersは、ユーザーがモデルの重要な属性を誤って更新してしまうことを防止するための、より優れたセキュリティ対策です。
https://railsguides.jp/action_controller_overview.html#strong-parameters
「Strong Parametersって何?初めて聞いたんですけど!」という方はRailsガイドを参照してください。
まとめ
この記事で説明した内容は以下の通りです。
- Railsのコントローラに出てくるparamsはハッシュではなく ActionController::Parameters オブジェクト
- paramsで参照しているのは変数ではなくメソッド
- ハッシュではないのでparamsから値を取り出すときはシンボルでも文字列でもOK
- paramsがハッシュになってないのはマスアサインメント脆弱性を防止するため
実際問題、Railsのコードを書くときにparamsがハッシュかそうでないかを意識する機会はそこまで多くありません。
しかし、「paramsはハッシュではない」ということを知っておくと、特殊な要件に対処したり、ややこしいエラーをデバッグしたりするときに役立つことがたまにあります。
また、他の人にparamsの役割を説明したりするときに「paramsはハッシュなんだよ」と教えてしまうと、微妙に間違った情報を伝えてしまうことになります。
そうではなく、「paramsはハッシュによく似たオブジェクトです」とか「paramsはハッシュじゃないけど、最初のうちはとりあえずハッシュと同じようなものと考えても良い」といった説明の方がベターですね。
もしみなさんの周りは「paramsはハッシュだ」と勘違いしている人がいたら、この記事をそっと差し出してみてください。
おまけ
この記事と同じテーマでChatGPTに記事を書かせてみました。
追記:Railsガイドの説明は・・・「paramsという名前のハッシュ」!!
本記事を書き追えて公開したあと、ふと思いました。
「そういえばRailsガイドではparamsのことをどういうふうに説明しているんだろう?」と。
調べました。
見つけました。
その結果はなんと・・・「paramsという名前のハッシュ」!!
Railsでは、パラメータをクエリ文字列で受け取ることもPOSTデータで受け取ることもできます。いずれの場合も、コントローラ内ではparamsという名前のハッシュでパラメータにアクセスできます。
Railsガイドはparamsをハッシュだと説明していました(びっくり)。
念のため本家の英語版も確認しておきましょう。
Rails does not make any distinction between query string parameters and POST parameters, and both are available in the params hash in your controller:
https://guides.rubyonrails.org/action_controller_overview.html
やはりこちらも"hash"だと説明されていました。
なるほど、これならたしかに「paramsはハッシュです」と説明するQiita/Zennの記事が続出するのも納得がいきます。
ということは、Railsの開発チームとしては「paramsはハッシュだと思ってくれたらいい」と考えているのかもしれません。
事実ベースで技術的な解説をするなら「paramsはハッシュではない」と断言して良いと思うのですが、Rails開発チームが「params = ハッシュと考えても別にええんやで」というスタンスなのであれば、この記事の説得力がちょっと低下してしまいますね😅