3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsで単一リソースのrouting(resource)を定義するときはダイレクトルーティングも検討する

Last updated at Posted at 2021-02-07

発生する問題

railsのroutingでresourceを定義しform_withヘルパーのmodel引数にモデルのインスタンスを指定するとエラーになる。

再現手順

1. resourceで単一のリソースのCRUDを作成する

config/routes.rb
Rails.application.routes.draw do
  resource :user
end

以下のpathが生成される

$ bundle exec rails routes | grep user
new_user  GET    /user/new(.:format)                                                                          users#new
edit_user GET    /user/edit(.:format)                                                                         users#edit
     user GET    /user(.:format)                                                                              users#show
          PATCH  /user(.:format)                                                                              users#update
          PUT    /user(.:format)                                                                              users#update
          DELETE /user(.:format)                                                                              users#destroy
          POST   /user(.:format)                                                                              users#create

2. viewでform_withにmodelを指定する

app/views/users/new.html.erb
<%= form_with(model: user) do |form| %>
<% end %>

3. 画面でフォームをサブミットする

エラーになる

undefined method `users_path' for #<ActionView::Base:0x00000000009420>
Did you mean?  user_path

原因

form_withメソッドはmodel引数に渡されたインスタンスのクラスからurlのpathを判定する。
この判定ロジックで判定されたpathと、単一リソースのrouting(resource)定義で生成されるpathの間で不整合が発生するためエラーが発生する。

原因の説明

1. form_withヘルパーのurl引数が存在しない場合、urlを判定するためにpolymorphic_pathを呼び出す

def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
  options[:allow_method_names_outside_object] = true
  options[:skip_default_ids] = !form_with_generates_ids

  if model
    url ||= polymorphic_path(model, format: format)

See: https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/form_helper.rb#L737-L742

2. polymorphic_urlHelperMethodBuilder.polymorphic_methodを呼び出す

def polymorphic_url(record_or_hash_or_array, options = {})
  if Hash === record_or_hash_or_array
    options = record_or_hash_or_array.merge(options)
    record  = options.delete :id
    return polymorphic_url record, options
  end

  if mapping = polymorphic_mapping(record_or_hash_or_array)
    return mapping.call(self, [record_or_hash_or_array, options], false)
  end

  opts   = options.dup
  action = opts.delete :action
  type   = opts.delete(:routing_type) || :url

  HelperMethodBuilder.polymorphic_method self,
                                         record_or_hash_or_array,
                                         action,
                                         type,
                                         opts
end

See: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb#L101-L121

3. HelperMethodBuilder.polymorphic_methodからbuilder.handle_modelが呼ばれる

def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
  builder = get action, type

  case record_or_hash_or_array
  .
  .
  else
    method, args = builder.handle_model record_or_hash_or_array
  end

See: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb#L205-L235

4. builder.handle_modelではmodel.persisted?trueの場合はuser_pathfalseの場合はusers_pathを返す

def handle_model(record)
  args  = []

  model = record.to_model
  named_route = if model.persisted?
    args << model
    get_method_for_string model.model_name.singular_route_key
  else
    get_method_for_class model
  end

  [named_route, args]
end

See: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb#L261-L273

$ model #=> User
$ get_method_for_string model.model_name.singular_route_key #=> "user_path"
$ get_method_for_class model #=> "users_path"

5. createアクションなので対象のmodelはまだ存在しないため、users_pathがかえり、urlが存在しないエラーになっている

対応

1. routesでダイレクトルーティングを指定する

以下のようにインスタンスのクラスがUserの場合はuser_pathに固定する。

config/routes.rb
Rails.application.routes.draw do
  resource :user
+ resolve('User') { [:user] }
end

2. form_withにurl引数を指定

app/views/users/new.html.erb
- <%= form_with(model: user) do |form| %>
+ <%= form_with(model: user, url: user_path) do |form| %>
<% end %>

参考

https://github.com/rails/rails/issues/1769
https://edgeguides.rubyonrails.org/routing.html#singular-resources

環境

rails 6.1.1

3
1
1

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?