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がとてもシンプルに

User.new(params[:user])

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

===

クエリストリングの解析はどうなっているのか、とRailsのコードを追ってみるとRack::Request クラスの GET メソッドに辿りつきます。

# rack/lib/rack/request.rb
module Rack
class Request
...
# Returns the data recieved in the query string.
def GET
if @env["rack.request.query_string"] == query_string
@env["rack.request.query_hash"]
else
@env["rack.request.query_string"] = query_string
@env["rack.request.query_hash"] =
Utils.parse_nested_query(query_string)
end
end
...


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


# rack/lib/rack/utils.rb
def parse_nested_query(qs, d = '&;')
params = {}

(qs || '').split(/[#{d}] */n).each do |p|
k, v = unescape(p).split('=', 2)
normalize_params(params, k, v)
end

return params
end
module_function :parse_nested_query

def normalize_params(params, name, v = nil)
name =~ %r([\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''

return if k.empty?

if after == ""
params[k] = v
elsif after == "[]"
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v)
else
params[k] << normalize_params({}, child_key, v)
end
else
params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v)
end

return params
end
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 件のコメント:

コメントを投稿