概要
railsではstrong parametersを用いて, 許可されていないパラメータによってモデルの重要な属性が誤って更新されてしまうことを防止することができます
パラメータの許可にはpermitを用います
また必須パラメータが不足している場合, requireやfetchを使うことで例外発生やデフォルトの値を設定することができます
さらにrails8からはパラメータの必須化と許可を同時に行うexpectが導入されました
この記事ではpermit, requireとfetchついて確認し, rails8で導入されたexpectについて見ていきます
permitについて
permitを用いることで許可するパラメータを決めることができます
スカラー, 配列, ハッシュなどで許可することができます
例として下記のようなparamsを用います
ActionController::Parameters.newで初期化したオブジェクトのpermitedはfalseとなっています
params = ActionController::Parameters.new({name: "name", numbers: [3, 5, 7], animal_hash: {dog: "wan", cat: "nya"}})
=> #<ActionController::Parameters {"name"=>"name", "numbers"=>[3, 5, 7], "animal_hash"=>{"dog"=>"wan", "cat"=>"nya"}} permitted: false>
スカラー
キーを指定することで取得できます
permited = params.permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>
permited[:name]
=> "name"
ハッシュの中のスカラーだけを取得することは出来ません
permited = params.permit(:dog)
=> #<ActionController::Parameters {} permitted: true>
permited.has_key?(:dog)
=> false
配列
キーを指定するだけでは取得出来ません
permited = params.permit(:numbers)
=> #<ActionController::Parameters {} permitted: true>
permited.has_key?(:numbers)
=> false
空配列を対応付ける必要があります
permited = params.permit(numbers: [])
=> #<ActionController::Parameters {"numbers"=>[3, 5, 7]} permitted: true>
permited.has_key?(:numbers)
=> true
permited[:numbers]
=> [3, 5, 7]
ハッシュ
ハッシュはキーを指定するだけ, また空配列を対応付けることで取得することはできません
permited = params.permit(:animal_hash)
=> #<ActionController::Parameters {} permitted: true>
permited.has_key?(:animal_hash)
=> false
permited = params.permit(animal_hash: [])
=> #<ActionController::Parameters {} permitted: true>
permited.has_key?(:animal_hash)
=> false
ハッシュで取得するときは空のハッシュを割り当てる必要があります
ハッシュで取得するときはActionController::Parametersのオブジェクトのままです
permited = params.permit(animal_hash: {})
=> #<ActionController::Parameters {"animal_hash"=>#<ActionController::Parameters {"dog"=>"wan", "cat"=>"nya"} permitted: true>} permitted: true>
permited.has_key?(:animal_hash)
=> true
permited[:animal_hash]
=> #<ActionController::Parameters {"dog"=>"wan", "cat"=>"nya"} permitted: true>
permited[:animal_hash].class
=> ActionController::Parameters
permited[:animal_hash][:dog]
=> "wan"
requireについて
requireで指定されたキーがない場合には例外が発生します
またpermitを実行しないと, permittedはfalseのままになります
つまりrequireではパラメータの許可は出来ません
さらにrequireで指定されたものはキーとして取得できなくなります
例として下記のparamsを用います
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
"title"と"content"を取得するためにはrequireに:article, permitに:title, :contentを渡します
permited = params.require(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
requireのキーで指定した:articleは取得することができなくなります
permited[:article]
=> nil
:titleと:contentは取得できます
permited[:title]
=> "title"
permited[:content]
=> "content"
requireで指定されたキー(:author)がパラメータにない場合, 例外が発生します
permited = params.require(:author).permit(:name)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
またrequireだけではpermittedはfalseのままなので, パラメータを許可するためにはpermitを実行する必要があります
not_permitted = params.require(:article)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>
not_permitted.permitted?
=> false
複数のキー(:article, :author)で構成されているものを考えます
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>
1つのキー(:article)を指定して取得することができます
permited = params.require(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
permited.keys
=> ["title", "content"]
複数のキーをrequireする場合には配列として渡してあげる必要があります
permited = params.require(:article, :author)
wrong number of arguments (given 2, expected 1) (ArgumentError)
permited = params.require([:article, :author])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>, #<ActionController::Parameters {"name"=>"name"} permitted: false>]
複数のキーでrequireを実行した場合, permitはキーごとに行う必要があります
permited = params.require([:article, :author]).permit(:title, :content, :name)
undefined method `permit` for an instance of Array (NoMethodError)
article_permited, author_permited = params.require([:article, :author])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>, #<ActionController::Parameters {"name"=>"name"} permitted: false>]
article_permited.permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
author_permited.permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>
fetchについて
requireと異なり, 例外ではなくデフォルトを設定することができます
fetchされたキーがなくなること, permitの実行が必要なところは一緒になります
例として下記を用います
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
requireと一緒でfetchされたキーがなくなることやpermitの実行が必要になります
permited = params.fetch(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
permited[:article]
=> nil
permited[:title]
=> "title"
permited[:content]
=> "content"
第2引数なしのfetchはrequireと同じ挙動になります
つまりfetchで指定されたキー(:author)がパラメータにない場合, 例外が発生します
permited = params.fetch(:author).permit(:name)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
第2引数でデフォルトを設定することができます
permited = params.fetch(:author, "author")
=> "author"
第2引数でスカラーを定義するとpermitは出来ません
permited = params.fetch(:author, "author").permit(:name)
undefined method `permit' for an instance of String (NoMethodError)
デフォルトにハッシュを指定することでpermitできるようになります
permited = params.fetch(:author, {name: "name"}).permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>
permited[:name]
=> "name"
permited = params.fetch(:author, {}).permit(:name)
=> #<ActionController::Parameters {} permitted: true>
permited[:name]
=> nil
requireとは異なり, 複数のキーを指定してfetchすることはできません
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>
# requireみたいに配列で取ってくることはできない
permited = params.fetch([:article, :author])
param is missing or the value is empty: [:article, :author] (ActionController::ParameterMissing)
# 第2引数は特に使われない
permited = params.fetch(:article, :author)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>
# 第2引数はデフォルトになってしまう
permited = params.fetch(:none, :author)
=> :author
permit, requireとfetchからexpectへ
Strong Parametersでパラメータの許可を行うときはpermit, パラメータの必須化を行うときはrequireとfetchが使われていました
しかしrails8ではパラメータの必須化と許可を同時に行うexpectが導入されました
これからはexpectをつかっていくことができます
expectについて
例として下記を用います
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
"title"と"content"を取得するためには下記のようになります
permitted = params.expect(article: [:title, :content])
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
:articleは取得できなくなり, :titleと:contentが取得できます
permitted[:article]
=> nil
constraints-project(dev)> permitted[:title]
=> "title"
constraints-project(dev)> permitted[:content]
=> "content"
また, パラメータの許可も行われています
permitted.permitted?
=> true
指定されたキーがない場合にはrequireと同様に例外が発生します
permitted = params.expect(:author)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
複数のキー(:article, :author)で構成されているものを考えます
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>
1つのキー(:article)を指定して取得することができます
permitted = params.expect(article: [:title, :content])
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
複数キー(:article, :author)をしてして取得することもできます
permitted = params.expect(article: [:title, :content], author: [:name])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>, #<ActionController::Parameters {"name"=>"name"} permitted: true>]
:authorのように1つの要素しかない場合には, 配列で渡さなくても実行できます
permitted = params.expect(article: [:title, :content], author: :name)
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>, #<ActionController::Parameters {"name"=>"name"} permitted: true
また, 下記のような複雑パラメータでも簡単に必須化と許可を行えます
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, authors: [{ name: "name1", age: 30}, {name: "name2", age: 35}] })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "authors"=>[{"name"=>"name1", "age"=>30}, {"name"=>"name2", "age"=>35}]} permitted: false>
キーの中の配列(:authors)も簡単に取得することが出来ます
permitted = params.expect(article: [:title, :content], authors: [[:name, :age]])
=>
[#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>,
...
permitted
=>
[#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>,
[#<ActionController::Parameters {"name"=>"name1", "age"=>30} permitted: true>, #<ActionController::Parameters {"name"=>"name2", "age"=>35} permitted: true>]]
rails8では二重配列構文[[:属性名]]という新しい配列マッチング構文が導入されています
まとめ
rails8でexpectが導入されたので簡単にまとめてみました
expectは必須化と許可を簡単に行うことが出来, 複雑なパラメータも簡単に扱えるようになりました
expcetはあらゆるパラメータフィルタに対応するわけではありませんが, 簡単に導入できるので試してみる価値がありそうです
ちなみに
今回の変更を取り扱っているプルリクエストは https://github.com/rails/rails/pull/51674 になります
rails8より前はパラメータの処理として
user_params = params.require(:user).permit(:name, :age)
のようにrequireの後にpermitを用いることが推奨されていました
しかしときには
user_params = params.permit(user: [:name, :age]).require(:user)
のように先にpermitする必要もありました
この問題などを解決するために今回の修正が入ったみたいです
参考
railsガイド: https://railsguides.jp/action_controller_overview.html#strong-parameters
ActionController::Parameters: https://api.rubyonrails.org/classes/ActionController/Parameters.html