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

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

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

### db/schema.rb

```ruby

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
```ruby

class Task < ApplicationRecord
  enum status_id: %i[undefined untouched in_prigress done]
end
```
### app/controllers/tasks_controller.rb
```ruby

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
```ruby

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
```ruby

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
``` 
以下がデバックした際のターミナル画面です。

```ruby

    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`の位置を変えて実行。

```ruby

    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`も

```ruby

[4] pry(#<TasksController>)> @task[:deadline].class
=> ActiveSupport::TimeWithZone
```
一瞬、えっ？っとなりましたがこれで問題ないようです。以下参照。
http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html

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

```ruby

[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`で入力して、取得した値は<strong>キーもバリューも全て`String`のパラメーターのハッシュ</strong>に格納される。（そりゃ、画面から送られてくる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に保存されている数値を取り出した際に、対応する文字列に変換して扱うことができることだと思っていました。

しかし実際は<strong>モデルを介してデータのやり取りをする際に数値と、それに対応する文字列を変換する</strong>ことが`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`でした。

## お願い

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