2009年8月4日火曜日

[Railsのキホン]リクエストパラメータ

知っている人には常識な話ですが、Railsでのリクエストパラメータの扱いについて。

Railsでは、ルーティング情報とクエリパラメータをハッシュマップ(パラメータ情報というらしい)に格納して、コントローラへ渡します。コントローラからは、params という変数でこのマップにアクセスします。
たとえば、"key"というリクエストパラメータを取得するには、 params[:key] と書きます。
※このとき、:key がルーティング定義(config/routes.rb)で使われていてはいけない。

ハッシュマップなので、ひとつのクエリパラメータ名に複数の値を対応させようとして、
http://foo.com/controller/action?key=val1&key=val2

とすると後の値(この場合 "val2" で params[:key] が上書きされてしまいます。
(仕組みを知らないとあわてる。。)

少しきちんとRailsのリクエストパラメータの受け渡しについて調べてみました。

参考:Railsを使ってURLパラメータの引き渡しと読み込みを行う方法

Railsではクエリパラメータのキーに、以下の3種類のデータ型をバインドすることができます。
  1. 文字列
  2. 配列
  3. ハッシュマップ

1. 文字列

ひとつのパラメータキーにひとつの値が対応する、基本形。
http://foo.com/params/show_param?key1=val1&key2=val2
とします。
パラメータ情報(params)は
{"key1"=>"val1", "amp"=>nil, "key2"=>"val2", "controller"=>"params", "action"=>"show_param"}

となる。

2. 配列
ひとつのパラメータキーに、複数の値を対応させたい場合の書式。
http://foo.com/params/show_param?key[]=val1&key[]=val2
のように、キーの後に [] をつけます。
パラメータ情報は
{"key"=>["val1", "val2"], "amp"=>nil, "controller"=>"params", "action"=>"show_param"}

となるので、params[:key][0], params[:key][1] としてアクセスできる。

3. ハッシュマップ
ひとつのパラメータキーに、ハッシュマップを対応させる場合の書式。Rails特有?
http://foo.com/params/show_param?user[name]=cocomo&user[gender]=F
のように、キーのあとにハッシュキーを[]で囲んで指定します。
パラメータ情報は
{"user"=>{"name"=>"cocomo", "gender"=>"F"}, "amp"=>nil, "controller"=>"params", "action"=>"show_param"}

となる。

ところで、ActiveRecordは new の引数にキーワード引数(ハッシュ)を渡すことができます。

それとこのパラメータの引渡し方と組み合わせることで、コントローラ内で、ActiveRecordのnewがとてもシンプルに
  1. User.new(params[:user])  

と書けることになります。(便利に使っていたけれど、こうなっていたのか。。。)

===

クエリストリングの解析はどうなっているのか、とRailsのコードを追ってみるとRack::Request クラスの GET メソッドに辿りつきます。
  1. # rack/lib/rack/request.rb  
  2. module Rack  
  3.   class Request  
  4.     ...  
  5.     # Returns the data recieved in the query string.  
  6.     def GET  
  7.       if @env["rack.request.query_string"] == query_string  
  8.         @env["rack.request.query_hash"]  
  9.       else  
  10.         @env["rack.request.query_string"] = query_string  
  11.         @env["rack.request.query_hash"]   =  
  12.           Utils.parse_nested_query(query_string)  
  13.       end  
  14.     end  
  15.     ...  


rack.request.query_hash が params の正体のようです。実際の解析処理はUtils.parse_nested_queryメソッドで行っているようなので、そっちを見てみます。

  1. # rack/lib/rack/utils.rb  
  2.   def parse_nested_query(qs, d = '&;')  
  3.       params = {}  
  4.   
  5.       (qs || '').split(/[#{d}] */n).each do |p|  
  6.         k, v = unescape(p).split('=', 2)  
  7.         normalize_params(params, k, v)  
  8.       end  
  9.   
  10.       return params  
  11.     end  
  12.     module_function :parse_nested_query  
  13.   
  14.     def normalize_params(params, name, v = nil)  
  15.       name =~ %r([\[\]]*([^\[\]]+)\]*)  
  16.       k = $1 || ''  
  17.       after = $' || ''  
  18.   
  19.       return if k.empty?  
  20.   
  21.       if after == ""  
  22.         params[k] = v  
  23.       elsif after == "[]"  
  24.         params[k] ||= []  
  25.         raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)  
  26.         params[k] << v  
  27.       elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)  
  28.         child_key = $1  
  29.         params[k] ||= []  
  30.         raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)  
  31.         if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)  
  32.           normalize_params(params[k].last, child_key, v)  
  33.         else  
  34.           params[k] << normalize_params({}, child_key, v)  
  35.         end  
  36.       else  
  37.         params[k] ||= {}  
  38.         raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)  
  39.         params[k] = normalize_params(params[k], after, v)  
  40.       end  
  41.   
  42.       return params  
  43.     end  
  44.     module_function :normalize_params  


ごちゃごちゃとしていますが、キーのあとが ""(空文字列)か、"[]"か、"[child_key]" かで場合分けをしていることが読み取れます。そして、どうやら(メソッド名が示すとおり)ネストしたパラメータキーもOKなようです。

ちょっと、実験。

その1。
クエリストリング
key[][]=val1&key[][]=val2
を与えた場合。
params は
{"key"=>[nil, nil], "controller"=>"params", "action"=>"show_param"}

となります。配列のネストは不可の様子。

その2。
user[name[first]]=John&user[name[family]]=Lennon
を与えた場合。
params は
{"user"=>{"name"=>{"first"=>"John", "family"=>"Lennon"}}, "controller"=>"params", "action"=>"show_param"}

となります。ちゃんと、ネストしたハッシュマップになっています。

その3。
child[][name]=Mai&child[][name]=Aki
を与えた場合。
params は
{"child"=>[{"name"=>"Mai"}, {"name"=>"Aki"}], "controller"=>"params", "action"=>"show_param"}

となります。配列の中にハッシュマップをネストさせてもOK。

その4。
person[child[]]=Mai&person[child[]]=Aki
とした場合。
params は
{"person"=>{"child"=>[nil, nil]}, "controller"=>"params", "action"=>"show_param"}

となります。ハッシュマップの中に配列をネストさせるのはできない様子(配列の値がnilになってしまっている)。

その5。
person[child[][name]]=Mai&person[child[][name]]=Aki
とした場合。
params は
{"person"=>{"child"=>[{"name"=>"Aki"}]}, "controller"=>"params", "action"=>"show_param"}

となります。child がハッシュマップの配列を指しているものの、最初のnameが2番目のnameで上書きされてしまっているため、これも意図したとおりにはなっていません。

# 要するに、配列はトップレベルでしか使えないということなんでしょうか。

0 件のコメント:

コメントを投稿