初めに
Formオブジェクトで日付などを保存する際に使われるヘルパーメソッド「date_select」で起こりやすいUnknownAttributeエラーの原因と解決方法について纏めました。
#目次
- Formオブジェクトとは
- ActiveModel::Model
- attr_accessor
- date_selectフォームヘルパ
- UnknownAttributeエラー
- ActiveRecord::AttributeAssignment
- 参考サイト
#Formオブジェクトとは
一つのフォームから複数のテーブルにデータを保存・更新する時に使われるものです。railsのモデルにはデータベースのテーブルに対応する情報が入っていますが、これらのテーブル情報を一まとめにしてくれるのがFromオブジェクトです。
例えば、日付を保存するsdate.rbとスケジュールを保存するschedule.rbという二つのモデルとそれに対応するテーブルがあるとします。
①-1 sdatesテーブル
①-2 sdate.rbのmigrationファイル(指定したカラム「:sdate,:syear,:smonth,:sday」)
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
②-1 scheduleテーブル
※subject,start_time,end_timeなどについては、未完成なので空欄になっています。また、null:false指定をしていないので、nullも保存できる+エラーも出ないようにしています。
②-2 schedule.rbのmigrationファイル(指定したカラム「:subject,:start_time,:end_time,:sdate」)
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
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オブジェクトのインスタンスを呼び出して保存するという形になります。
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」というふうに外部キーとして保存します。
④コントローラ(一部)
2つのモデルsdate.rbとschedule.rbのどちらかにコントローラを用意し、Fromオブジェクトを実行するようにします。今回の場合、schedule.rbに対応するshcedulesコントローラで処理しました。
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フォームヘルパを使いました。
<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 %>
これでうまくいくはずだと思いきや、次のようなエラーが出ました。
UnknownAttributeエラー
UnknownAttributeエラーとはつまり未知の属性ですよという意味になります。入力フォームからparameterとしてデータが送られてきたものの、該当するデータがないということです。なぜこのようなことが起こるのでしょうか。原因を調べるためにparamsと入力し中身をみてみました。
requestとして送られているparametersには先ほど入力した日付のパラメーターがちゃんと届いています。しかし、送られてきたパラメーターの形がちょっと違います。 「"sdate(1i)"=>"2021", "sdate(2i)"=>"2", "sdate(3i)"=>"1"」というふうにsdateカラムを年「sdate(1i)」、月「sdate(2i)」、日「sdate(3i)」に分解してきています。sdateテーブルにはsdateカラムしかないのに3つもカラムを分けて届いているから保存ができなかったんですね。
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)などでバラバラになっている属性値を一塊にして渡してくれます。
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