はじめに
先日、一覧を取得するページで新規レコードも登録できるフォームを設置する必要に迫られたのですが、その時Railsが自分としては意外な動きをする部分があったので紹介します。
完成形イメージ
-
User
とTask
モデルは1対多の関係 -
Task
の一覧を表示しつつ、同じページに新規タスクの入力フォームを設置
エラーが発生するコード
def index
current_user = User.find(params[:user_id])
@tasks = current_user.tasks
@task = @tasks.new
end
@task
の書き方を変えた下記でも同じです。
def index
current_user = User.find(params[:user_id])
@tasks = current_user.tasks
@task = current_user.tasks.new
end
発生したエラー
上記のように書いたところView側でエラーが発生しました。
ActionController::UrlGenerationError in Tasks#index
No route matches {:action=>"show", :controller=>"tasks", :id=>nil, :user_id=>"2"}, missing required keys: [:id]
id
がnil
で、タスク詳細へのリンクを作れない、というエラーでした。
原因はタスクの一覧データに新規インスタンスを含んでしまっていたこと
調べると、原因は、
コントローラで@task = @tasks.new
をしたタイミングで@tasks
に新規インスタンスが突っ込まれてしまうこと
でした。
def index
@tasks = current_user.tasks
@task = @tasks.new
# ここで@tasksを見ると、↑でnewしたインスタンスが入っている.
# @tasks => <ActiveRecord::Associations::CollectionProxy [#<...略..., #<Task id: nil, user_id: 2, created_at: nil, updated_at: nil, title: nil>]>
end
一覧を表示するときに@tasks
をeach
で回して各レコードのデータを表示しているのですが、その@tasks
にnew
したインスタンスが入ってしまっていて、バグが混入するという具合です。
これは、関連づけがない場合には起こらないです。
(ActiveRecord::Associations::CollectionProxy
あたりの仕組みを調べれば分かりそうな感じがしますが、根本的な究明まではできていません。)
例えば、以下のコードは何の問題もなく動きます。
def index
@tasks = Task.all
@task = @tasks.new
# @tasksにnewしたインスタンスは入っていない
end
補足:current_user
をprivateメソッドで生成すると少し結果が変わる
この記事を書こうと思って色々触っていて気づいたのですが、先述のコードで、current_user
を変数ではなくprivateメソッドで生成すると結果が少し変わりました。
下記の書き方だと変わらず失敗します。
def index
@tasks = current_user.tasks
@task = @tasks.new
# この時点で、@tasksにnewしたインスタンスが入ってしまう
end
private
def current_user
User.find(params[:user_id])
end
しかし、@task
の定義を変えると、今度は成功するようになります。
def index
@tasks = current_user.tasks
@task = current_user.tasks.new
# @tasksにnewしたインスタンスは入らない
end
private
def current_user
User.find(params[:user_id])
end
この書き方だと、current_user
をその都度作るので、@tasks
にnew
したインスタンスが含まれないということのようです。
バグの回避方法
このバグの回避方法は簡単です。どちらかの定義について、関連づけのさせ方を変えてあげれば良いです。
いくつか紹介します。
①Task.new
を使う
def index
current_user = User.find(params[:user_id])
@tasks = current_user.tasks
@task = Task.new(user: current_user)
end
②Task.where
を使う
def index
current_user = User.find(params[:user_id])
@tasks = Task.where(user: current_user)
@task = @tasks.new
end
③@tasks
を配列にする
以降の処理で、ActiveRecordである必要が必ずしもない場合は、配列にするという方法もあります。
def index
current_user = User.find(params[:user_id])
@tasks = current_user.tasks.to_a
@task = current_user.tasks.new
end
さいごに
本当はなぜこうなるのか、という仕組みの部分まで究明したかったのですが、今回はそこまではできなかったです。
もしかしたら「そりゃそうなるでしょ」という話なのかもしれないですが、自分としては知らない挙動で勉強になりました。