【Rails】Formオブジェクトでdate_selectヘルパーを利用した時に起こりえるUnknownAttributeエラーについて

# 初めに
Formオブジェクトで日付などを保存する際に使われるヘルパーメソッド「date_select」で起こりやすいUnknownAttributeエラーの原因と解決方法について纏めました。

#目次
- Formオブジェクトとは
- ActiveModel::Model
- attr_accessor
- date_selectフォームヘルパ
- UnknownAttributeエラー
- ActiveRecord::AttributeAssignment
- 参考サイト

#Formオブジェクトとは
一つのフォームから複数のテーブルにデータを保存・更新する時に使われるものです。railsのモデルにはデータベースのテーブルに対応する情報が入っていますが、これらのテーブル情報を一まとめにしてくれるのがFromオブジェクトです。
例えば、日付を保存するsdate.rbとスケジュールを保存するschedule.rbという二つのモデルとそれに対応するテーブルがあるとします。 

①sdate.rb
![スクリーンショット 2021-02-01 11.35.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/c579d6fd-7320-5e98-dd7e-572806c63fc9.png)

①-1 sdatesテーブル
![スクリーンショット 2021-02-01 11.37.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/31fdbc7d-8f59-e9b1-83cb-b64698f00642.png)
①-2 sdate.rbのmigrationファイル（指定したカラム「:sdate,:syear,:smonth,:sday」）

```ruby
class CreateSdates < ActiveRecord::Migration[6.0]
  def change
    create_table :sdates do |t|
      t.date :sdate 
      t.integer :syear
      t.integer :smonth
      t.integer :sday
      t.timestamps
    end
  end
end
```
②schedule.rb
![スクリーンショット 2021-02-01 11.39.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/c53fe939-ad4d-a143-673c-a2aa050a7469.png)
②-1 scheduleテーブル
![スクリーンショット 2021-02-01 11.41.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/2d97015f-4b2b-47b5-259e-d1df53033ca0.png)
※subject,start_time,end_timeなどについては、未完成なので空欄になっています。また、null:false指定をしていないので、nullも保存できる＋エラーも出ないようにしています。
②-2 schedule.rbのmigrationファイル（指定したカラム「:subject,:start_time,:end_time,:sdate」）

```ruby

class CreateSchedules < ActiveRecord::Migration[6.0]
  def change
    create_table :schedules do |t|
      t.string :subject
      t.time :start_time
      t.time :end_time
      t.references :sdate, foreign_key: true
      t.timestamps
    end
  end
end
```

③上記①と②を纏めてくれるFormオブジェクトを作成 -> sdate_schedule.rb

```ruby

class SdateSchedule
  include ActiveModel::Model
  attr_accessor :subject, :start_time, :end_time, :sdate, :syear, :smonth, :sday

  def save
    sydate = Sdate.create(sdate: sdate ,syear: syear, smonth: smonth, sday: sday)
    Schedule.create(subject: subject, start_time: start_time, end_time: end_time, sdate_id: sydate.id)
  end

end
```
# ActiveModel::Model
FormオブジェクトはRubyファイルにクラスを作り（今回の場合SdateScheduleというクラス名になります）、その中でsaveメソッドを定義します。このsaveメソッドを定義することにより、コントローラでSdateScheduleクラスのインスタンスを生成しsaveメソッドを記述するとデータが保存される仕組みになっています。この時にモデルの生成、保存・更新などを可能にしてくれるのが「ActiveModel::Model」になります。これをincludeでSdateScheduleで使えるようにする必要があります。

# attr_accessor
クラス内で定義したインスタンス変数をクラスの外でも変更・保存などができるようにするためし指定するものです。Formオブジェクトでattr_accessorを利用し、保存される変数を指定しておかないとクラスの外、ここではコントローラで値を書き込むことができなくなります。入力フォームからデータはparameterとして送られてきますが、コントローラでストロングパラメーターとして受け取る処理をした後に、Formオブジェクトのインスタンスを呼び出して保存するという形になります。

```ruby
  def save
    sydate = Sdate.create(sdate: sdate ,syear: syear, smonth: smonth, sday: sday)
    Schedule.create(subject: subject, start_time: start_time, end_time: end_time, sdate_id: sydate.id)
  end
```
変数sydateにSdateモデルの各値をActiveModel::Modelのcreateメソッドで保存し代入します。Scheduleモデルも同じようにモデルの値をcreateメソッドで保存します。Sdateモデルと違う点はScheduleはSdateの外部キーも保存しなければならないので、上記のsydateのid情報も「sdate_id: sydate.id」というふうに外部キーとして保存します。

④コントローラ(一部）
２つのモデルsdate.rbとschedule.rbのどちらかにコントローラを用意し、Fromオブジェクトを実行するようにします。今回の場合、schedule.rbに対応するshcedulesコントローラで処理しました。

```ruby
class SchedulesController < ApplicationController
（・・・前略）

def new
  @sdate_schedule = SdateSchedule.new
end

def create
  @sdate_schedule = SdateSchedule.new(schedule_params)
  @sdate_schedule.save
  redirect_to schedules_path
end

private

def schedule_params
  params.require(:sdate_schedule).permit(:subject, :start_time, :end_time, :sdate, :syear, :smonth, :sday)
end

```
schedule_paramsというメソッドでパラメーターを受け取り、newメソッドでインスタンス@sdate_scheduleを生成します。createメソッドでは生成したインスタンスにschedule_paramsを渡し、Formオブジェクトで定義したsaveメソッドを使いデータベースにデータを保存するようにしています。

#date_selectフォームヘルパ
入力フォームに日付を入力し、保存できるようにするために使うのがdate_selectです。今回の場合、sdateモデルにdate型のデータを保存するためにdate_selectフォームヘルパを使いました。

```html
<h1>授業新規登録</h1>
<%= form_with model: @sdate_schedule, url: schedules_path, local: true do |f| %>
  <%= raw sprintf(
                  f.date_select(
                                :sdate,
                                {:discard_year => true,:use_month_numbers => 
                                 true,date_separator: '%s'},
                               ),
                                "<p> 月 </p>") + "<p> 日 </p>" %>
   <%= f.submit "新規登録" ,class:"#" %>
<% end %>
```

入力フォームで日付を入力し、新規登録ボタンを押します。
![スクリーンショット 2021-02-01 12.51.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/84f7f77f-07b9-3191-b079-08108f4453a9.png)

これでうまくいくはずだと思いきや、次のようなエラーが出ました。
![スクリーンショット 2021-02-01 12.53.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/9c6ec75c-680f-1b85-0066-5dd459858744.png)

# UnknownAttributeエラー
UnknownAttributeエラーとはつまり未知の属性ですよという意味になります。入力フォームからparameterとしてデータが送られてきたものの、該当するデータがないということです。なぜこのようなことが起こるのでしょうか。原因を調べるためにparamsと入力し中身をみてみました。

![スクリーンショット 2021-02-01 12.57.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/4a2a45d5-1d3a-4c57-46b6-148e451947bf.png)
requestとして送られているparametersには先ほど入力した日付のパラメーターがちゃんと届いています。しかし、送られてきたパラメーターの形がちょっと違います。 「"sdate(1i)"=>"2021", "sdate(2i)"=>"2", "sdate(3i)"=>"1"」というふうにsdateカラムを年「sdate(1i)」、月「sdate(2i)」、日「sdate(3i)」に分解してきています。sdateテーブルにはsdateカラムしかないのに３つもカラムを分けて届いているから保存ができなかったんですね。

# ActiveRecord::AttributeAssignment
これを解決してくれるのが「ActiveRecord::AttributeAssignment」です。これは公式サイトで確認すると「Allows you to set all the attributes by passing in a hash of attributes with keys matching the attribute names.」となっています。つまり、属性名と一致するキーを持つ属性の塊を渡し、そのすべての属性を設定できるとなっています。このActiveRecord::AttributeAssignmentをFromオブジェクトに記述するとsdate(1i)などでバラバラになっている属性値を一塊にして渡してくれます。
![スクリーンショット 2021-02-01 13.09.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966135/f82ddca7-4123-3401-d428-10c9ee1c546a.png)
sdate_schedule.rbに「include ActiveRecord::AttributeAssignment」を追記します。
これでエラーが解消し、テーブルに保存できるようになりました。

- 参考サイト 

https://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html#method-i-assign_attributes
https://railsguides.jp/active_model_basics.html
https://qiita.com/Tiima/items/b14c73df98d0465cbb52
https://naokirin.hatenablog.com/entry/2019/02/20/231317
https://qiita.com/ngron/items/dd3cd8eb8ef58bd1c1fc