LoginSignup
41
31

More than 5 years have passed since last update.

enumを使うとArgumentError in TasksController#create => '2' is not a valid status_id

Last updated at Posted at 2018-05-25

久しぶりに遭遇しました。railsenumを使う時に起きるこのエラー。
今回が2回目の遭遇なのですが、前回は根本的な解決をしないまま小手先の変更で退却してしまいました。
なので今回はこのエラーとじっくり向き合ってみようと思います。

現在の状況

タスクを管理するアプリケーションを作成中です。
Taskモデルのstatus_idカラムをInteger型保存して、
ビューで表示する際にenumを利用して数値に対応したステータスを表示させたいと考えています。
以下コードになります。

db/schema.rb


ActiveRecord::Schema.define(version: 20180516232642) do
  create_table "tasks", force: :cascade do |t|
    t.string "title", null: false
    t.text "detail", default: "detail", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.datetime "deadline"
    t.integer "status_id"
  end
end

app/models/task.rb


class Task < ApplicationRecord
  enum status_id: %i[undefined untouched in_prigress done]
end

app/controllers/tasks_controller.rb


class TasksController < ApplicationController

   ~一部省略~

  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to root_path, notice: 'Succeeded to save the task!!'
    else
      render 'new'
    end
  end

  def update
    if @task.update(task_params)
      redirect_to root_path, notice: 'Succeeded to save the task!!'
    else
      render 'edit'
    end
  end

  private

  def task_params
    params.require(:task).permit(:title, :detail, :deadline, :status_id)
  end
end

app/helpers/tasks_helper.rb


module TasksHelper

  def deadline_format(datetime)
    datetime.present? ? datetime.strftime('%Y.%m.%d') : t(:undefined)
  end

  def status_format(status)
    status.presence ||  t(:undefined)
  end
end

app/views/tasks/_form.slim

= form_for(@task) do |f|
  .form_title
    = f.text_field :title, placeholder: t(:title), class: "form_title__text_field"
  .form_detail
    = f.text_area :detail, placeholder: t(:detail), class: "form_detail__text_area"
  .form_name
    .form_name__font = t(:deadline)
  .form_deadline
    = f.date_select :deadline, class: "form_deadline_select"
  .form_status
    = f.select(:status_id, {undefined: 0, untouched: 1, in_progress: 2, done: 3}, { class: "form_status__select" })
  .form_submit
    = f.submit t(:submit), class: "form_submit__button"

タスクを新規で保存する際も、既存のタスクを更新する際も同じフォームを利用しています。
tasks/new.slimviews/tasks/edit.slimの両方で同じviews/tasks/_form.slimrenderしています。)

app/views/tasks/index.slim

  tbody.task_table__body
    - @tasks.each.with_index do |task, n|
      tr.task_table__line{ id = n }
        td.task_table__line__priority = t(:s)
        td.task_table__line__deadline = deadline_format(task.deadline)
        td.task_table__line__title = task.title
        td.task_table__line__detail = task.detail
        td.task_table__line__status = status_format(task.status_id)

問題点

この状態で新規タスクの作成と既存のタスクの更新を行うと以下のようなエラーが出てきます。

Completed 500 Internal Server Error in 4ms (ActiveRecord: 0.7ms)

ArgumentError ('2' is not a valid status_id):

エラーの意味は理解できます。
引数としてのstatus_idが無効な値です。そして'2' is invalidと表示されているので、
おそらくIntegerで取得されるべき値がStringで来ています、とのことだと思います。

試しにapp/models/task.rbenumの記述をコメントアウトした状態で同じ動作を行ってみましたが、エラーは出てこなかったのでenumに問題があるようです。

試行錯誤

先ず、本当にstatus_idString型で取得されているのかをbinding.pryを使って調べてみました。
(あくまで、型を調べるためなのでエラーを回避するためにapp/models/task.rbenumの記述をコメントアウトした状態で行いました。)

app/controllers/tasks_controller.rb


class TasksController < ApplicationController

   ~~一部省略~~

  def create
    bindung.pry
    @task = Task.new(task_params)
    if @task.save
      redirect_to root_path, notice: 'Succeeded to save the task!!'
    else
      render 'new'
    end
  end

  def update
    if @task.update(task_params)
      redirect_to root_path, notice: 'Succeeded to save the task!!'
    else
      render 'edit'
    end
  end

  private

  def task_params
    params.require(:task).permit(:title, :detail, :deadline, :status_id)
  end
end

以下がデバックした際のターミナル画面です。


    14: def create
    15:   binding.pry
 => 16:   @task = Task.new(task_params)
    17:   if @task.save
    18:     redirect_to root_path, notice: 'Succeeded to save the task!!'
    19:   else
    20:     render 'new'
    21:     return
    22:   end
    23: end

[1] pry(#<TasksController>)> task_params
=> <ActionController::Parameters {"title"=>"test for status", "detail"=>"detail", "deadline(1i)"=>"2018", "deadline(2i)"=>"5", "deadline(3i)"=>"17", "status_id"=>"2"} permitted: true>
[2] pry(#<TasksController>)> status = task_params[:status_id]
=> "2"
[3] pry(#<TasksController>)> status.class
=> String

やっぱり…
そしてdatetime型であるはずのdeadlinestringになっている…

今度はbinding.pryの位置を変えて実行。


    14: def create
    15:   @task = Task.new(task_params)
    16:   binding.pry
 => 17:   if @task.save
    18:     redirect_to root_path, notice: 'Succeeded to save the task!!'
    19:   else
    20:     render 'new'
    21:     return
    22:   end
    23: end

[1] pry(#<TasksController>)> @task
=> #<Task:0x00007f981adc0398
 id: nil,
 title: "明日の夕方、海岸へ行く",
 detail: "detail",
 created_at: nil,
 updated_at: nil,
 deadline: Fri, 18 May 2018 00:00:00 JST +09:00,
 status_id: 1>
[2] pry(#<TasksController>)> @task[:status_id]
=> 1
[3] pry(#<TasksController>)> @task[:status_id].class
=> Integer

ちゃんとintegerになっていました。
deadline


[4] pry(#<TasksController>)> @task[:deadline].class
=> ActiveSupport::TimeWithZone

一瞬、えっ?っとなりましたがこれで問題ないようです。以下参照。
http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html

では、task_params自体はどのようになっているのだろうか。


[5] pry(#<TasksController>)> task_params
=> <ActionController::Parameters {"title"=>"TEST", "detail"=>"detail", "deadline(1i)"=>"2018", "deadline(2i)"=>"5", "deadline(3i)"=>"19", "status_id"=>"2"} permitted: true>
[6] pry(#<TasksController>)> task_params[:status_id].class
=> String

こちらは変わらずstringのまま。

推測

上記の試行錯誤を基に@taskに値が代入される流れを確認。
※まだActionControllerのソースをあまり理解できないのであくまで推測に近いです。

form_forで入力して、取得した値はキーもバリューも全てStringのパラメーターのハッシュに格納される。(そりゃ、画面から送られてくるPOSTデータには型情報が無いですもんね…)

Taskクラスのインスタンスを作成する際にActiveRecorddb/schema.rbを参照して、キーがシンボルのカラム名のハッシュオブジェクトを作成する。(引数なしのnewメソッドの後にbinding.pryを実行して確認。)

ActiveRecordがパラメーターのハッシュのバリューをdb/schema.rbに基づいて適切な形(データ型)に変換して、Taskクラスのインスタンスの該当キーのバリューとして代入する。

そして③が実行される際に、本来Integerであるはずののstatus_idStringのままでハッシュに渡されているからエラーが起こると思っていました。

けど、よく考えたらそんなことあり得ないです。deadlineはちゃんとActiveSupport::TimeWithZoneクラスのオブジェクトに変換されているのだから。

ということは、enumに関して根幹的に誤解している可能性が出てきました。

enumに関する勘違い

結論から言うと自分は今までenumの機能は、DBに保存されている数値を取り出した際に、対応する文字列に変換して扱うことができることだと思っていました。

しかし実際はモデルを介してデータのやり取りをする際に数値と、それに対応する文字列を変換することがenumの機能なのです。(日本語が分かり辛くてすいません。要は数値から文字列への変換を一方通行で行うわけじゃないよ、ということです。)

つまり、値がモデルクラスのインスタンスに代入される際も、enumで設定した値しか受け付けないと言うことです。と言うことはただ単に、フォームで設定するバリューをenumで設定している文字列に変えれば良いだけ。そもそもエラーを読んだ時点で認識を誤っていました。

解決

フォームを以下のように編集すればOK

app/views/tasks/_form.slim

= form_for(@task) do |f|
  .form_title
    = f.text_field :title, placeholder: t(:title), class: "form_title__text_field"
  .form_detail
    = f.text_area :detail, placeholder: t(:detail), class: "form_detail__text_area"
  .form_name
    .form_name__font = t(:deadline)
  .form_deadline
    = f.date_select :deadline, class: "form_deadline_select"
  .form_status
    = f.select(:status_id, {undefined: 'undefined', untouched: 'untouched', in_progress: 'in_progress', done: 'done'}, { class: "form_status__select" })
  .form_submit
    = f.submit t(:submit), class: "form_submit__button"

以上、たかがenumされどenumでした。

お願い

記事の内容に誤りがありましたらコメントしていただけるとありがたいです。またご意見、ご質問等がある方もコメントをしていただければと思います。よろしくお願い致します。

41
31
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
41
31