久しぶりに遭遇しました。rails
でenum
を使う時に起きるこのエラー。
今回が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.slim
とviews/tasks/edit.slim
の両方で同じviews/tasks/_form.slim
をrender
しています。)
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.rb
のenum
の記述をコメントアウトした状態で同じ動作を行ってみましたが、エラーは出てこなかったのでenum
に問題があるようです。
試行錯誤
先ず、本当にstatus_id
がString
型で取得されているのかをbinding.pry
を使って調べてみました。
(あくまで、型を調べるためなのでエラーを回避するためにapp/models/task.rb
のenum
の記述をコメントアウトした状態で行いました。)
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
型であるはずのdeadline
もstring
になっている…
今度は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
クラスのインスタンスを作成する際にActiveRecord
がdb/schema.rb
を参照して、キーがシンボルのカラム名のハッシュオブジェクトを作成する。(引数なしのnew
メソッドの後にbinding.pry
を実行して確認。)
③ActiveRecord
がパラメーターのハッシュのバリューをdb/schema.rb
に基づいて適切な形(データ型)に変換して、Task
クラスのインスタンスの該当キーのバリューとして代入する。
そして③が実行される際に、本来Integer
であるはずののstatus_id
がString
のままでハッシュに渡されているからエラーが起こると思っていました。
けど、よく考えたらそんなことあり得ないです。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
でした。
お願い
記事の内容に誤りがありましたらコメントしていただけるとありがたいです。またご意見、ご質問等がある方もコメントをしていただければと思います。よろしくお願い致します。