5
0

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 1 year has passed since last update.

Ruby開発Advent Calendar 2022

Day 22

【Rails】インスタンス一覧と新規インスタンスを同時に取得するときの挙動が少し意外だった

Last updated at Posted at 2022-12-20

はじめに

先日、一覧を取得するページで新規レコードも登録できるフォームを設置する必要に迫られたのですが、その時Railsが自分としては意外な動きをする部分があったので紹介します。

完成形イメージ

  • UserTaskモデルは1対多の関係
  • Taskの一覧を表示しつつ、同じページに新規タスクの入力フォームを設置

image.png

エラーが発生するコード

tasks_controller.rb
def index
  current_user = User.find(params[:user_id])
  @tasks = current_user.tasks
  @task = @tasks.new
end

@taskの書き方を変えた下記でも同じです。

tasks_controller.rb
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]

idnilで、タスク詳細へのリンクを作れない、というエラーでした。

原因はタスクの一覧データに新規インスタンスを含んでしまっていたこと

調べると、原因は、
コントローラで@task = @tasks.newをしたタイミングで@tasksに新規インスタンスが突っ込まれてしまうこと
でした。

tasks_controller.rb
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

一覧を表示するときに@taskseachで回して各レコードのデータを表示しているのですが、その@tasksnewしたインスタンスが入ってしまっていて、バグが混入するという具合です。

これは、関連づけがない場合には起こらないです。
ActiveRecord::Associations::CollectionProxyあたりの仕組みを調べれば分かりそうな感じがしますが、根本的な究明まではできていません。)
例えば、以下のコードは何の問題もなく動きます。

tasks_controller.rb
def index
  @tasks = Task.all
  @task = @tasks.new
  # @tasksにnewしたインスタンスは入っていない
end

補足:current_userをprivateメソッドで生成すると少し結果が変わる

この記事を書こうと思って色々触っていて気づいたのですが、先述のコードで、current_userを変数ではなくprivateメソッドで生成すると結果が少し変わりました。

下記の書き方だと変わらず失敗します。

tasks_controller.rb
def index
  @tasks = current_user.tasks
  @task = @tasks.new
  # この時点で、@tasksにnewしたインスタンスが入ってしまう
end

private

def current_user
  User.find(params[:user_id])
end

しかし、@taskの定義を変えると、今度は成功するようになります。

tasks_controller.rb
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をその都度作るので、@tasksnewしたインスタンスが含まれないということのようです。

バグの回避方法

このバグの回避方法は簡単です。どちらかの定義について、関連づけのさせ方を変えてあげれば良いです。
いくつか紹介します。

Task.newを使う

tasks_controller.rb
def index
  current_user = User.find(params[:user_id])
  @tasks = current_user.tasks
  @task = Task.new(user: current_user)
end

Task.whereを使う

tasks_controller.rb
def index
  current_user = User.find(params[:user_id])
  @tasks = Task.where(user: current_user)
  @task = @tasks.new
end

@tasksを配列にする
以降の処理で、ActiveRecordである必要が必ずしもない場合は、配列にするという方法もあります。

tasks_controller.rb
def index
  current_user = User.find(params[:user_id])
  @tasks = current_user.tasks.to_a
  @task = current_user.tasks.new
end

さいごに

本当はなぜこうなるのか、という仕組みの部分まで究明したかったのですが、今回はそこまではできなかったです。
もしかしたら「そりゃそうなるでしょ」という話なのかもしれないですが、自分としては知らない挙動で勉強になりました。

5
0
0

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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?