#はじめに
現在、Ruby on Railsにて睡眠を記録するアプリを作成しています。form_withを使用して日時を記録する際に、登録したデータの日付が2000年1月1日としか保存されませんでした。非常に単純な原因でしたので、備忘録として投稿します。
開発環境
DBにはMySQLを使用しています。
- Mac OS Big Sur 11.6
- VSCode
- Ruby 3.0.2p107
- Rails 6.1.4.1
- MySQL 8.0.27 for macos11.6 on arm64 (Homebrew)
#原因
- migrationファイルでDateTime型とすべきところをTime型にしていた。
- MySQLについて調べるべきなのに、Rubyのことを調べていた。
背後要因
日付だけでなく時刻も扱える Date のサブクラスです。
DateTime は deprecated とされているため、 Timeを使うことを推奨します。
Ruby 3.0.0 リファレンスマニュアル class DateTime(要約)
RubyのTime型について調べたときに、リファレンスを見て「なるほど、Ruby3.0以降はDateTimeは非推奨なのか。ならばmigrationを作るときもTime型を使えばいいのか!」と安易な考えに至ってしまいました。
やりたかったこと
やりたかったことは、「フォームを利用して、就寝した時間と起床した時間をDBに保存できるかを確かめるための試験的な機能を作る」ことでした。
- 登録画面で就寝および起床時間を選択
- 登録ボタンを押したらデータをDBに保存
- 正常に保存できたら睡眠データの一覧ページにリダイレクト
このような機能を目指して作成していました。
migration
すでに生成してあるUserモデルと関連を持たせています。UserとSleepLogは、1対多の関係になります。
class CreateSleepLogs < ActiveRecord::Migration[6.1]
def change
create_table :sleep_logs do |t|
t.references :user, foreign_key: true
t.time :sleep_at
t.time :wake_at
t.timestamps
end
add_index :sleep_logs, %i[user_id created_at]
end
end
なお、ここで問題の原因となったのがsleep_atカラムとwake_atカラムです。本来はここでdatetime型とすべきところを、time型としてしまいました。
routes
ルーティングはresourcesを利用してindex以外のアクションを生成しています。
resources :sleep_logs, except: :index
これでsleep_logs_pathにPOSTすればsleep_logs_controllerのcreateアクションが利用できるようになりました。
$ rails routes | grep sleep_logs
sleep_logs POST /sleep_logs(.:format) sleep_logs#create
controller
データを正常に保存できれば、ユーザーの個別ページで睡眠データ一覧を表示するようにしています。
class SleepLogsController < ApplicationController
def new
end
def create
sleep_log = current_user.sleep_logs.build(sleep_log_params)
if sleep_log.save
flash.now[:success] = "記録が保存されました"
redirect_to @current_user
else
flash[:invalid] = "エラーが発生しました"
render 'new'
end
end
(省略)
private
def sleep_log_params
params.require(:sleep_log).permit(:sleep_at, :wake_at)
end
end
view
登録用フォームのviewです。form_withのtime_selelctを利用して、時間を選択できるようにしています。とりあえず試験的なデータを保存できればいいのでdefaultオプションを設定しています。
<%= form_with(url: sleep_logs_path, scope: :sleep_log, local: true) do |f| %>
<%= f.label :sleep_at, "就寝時間" %>
<%= f.time_select :sleep_at, default: Time.zone.now - 8.hours %>
<%= f.label :wake_at, "起床時間" %>
<%= f.time_select :wake_at, default: Time.zone.now %>
<%= f.submit "記録する" %>
<% end %>
特にcssなどは当てていないため、chrome上ではこのように表示されます。
睡眠データ一覧ページのviewについては省略させていただきます。
実際に登録してみる
とりあえず準備が整ったので、睡眠データを保存してみます。
登録前
登録後
登録したデータ自体のid、関連付けしてあるuserのuser_id、日付、就寝時間、起床時間の順に表示されています。
投稿内容を編集している現在(2021/12/03)の日付を表示したいのですが、01/02となってしまっています。
どんな日付が入っているのか?
rails consoleを利用して、睡眠データの中身を確認してみます。sleep_atとwake_atは2000年の1月、timestamp(created_atとupdated_at)は正しく時間が表示されています。
[#<SleepLog:0x000000012ef4dc90
id: 1,
user_id: 1,
sleep_at: Sun, 02 Jan 2000 07:50:00.000000000 JST +09:00,
wake_at: Sat, 01 Jan 2000 15:50:00.000000000 JST +09:00,
created_at: Fri, 03 Dec 2021 15:50:41.277532000 JST +09:00,
updated_at: Fri, 03 Dec 2021 15:50:41.277532000 JST +09:00>]
paramsの内容を調べてみる
原因を探るため、まずは「paramsに正しいデータが保存されているか」を調べてみることにしました。controllerにraiseを追加して、わざと例外を発生させてみます。
def create
sleep_log = current_user.sleep_logs.build(sleep_log_params)
raise
if sleep_log.save
flash.now[:success] = "記録が保存されました"
redirect_to @current_user
else
flash[:invalid] = "エラーが発生しました"
render 'new'
end
end
paramsの中身は...?
このようになっていました。どうやらparams[:sleep_log]内には、sleep_atとwake_atそれぞれの年、月、日、時、分が格納されているようです。年には2021、月には12, 日には3が入っています。paramsには問題なさそうです。
問題の解決
いろいろと検索してみた結果、「DBの型がおかしいのかもしれない」と勘付きました。RubyのTime型では日付も扱うことができますが、MySQLのTime型では時間を取り扱うだけで日付は保存できないようですね…
migrationの修正
sleep_atをwake_atをdatetime型に変換し、rails db:migrate:resetを実行します。
t.datetime :sleep_at
t.datetime :wake_at
##再度、登録してみる
フォームを利用して再度データを登録してみます。
DateTime型に変更した結果、日付も正しく保存されるようになりました!!
最後に
問題が発生したときは、何が原因かを探る前に「何を取り扱っているのか」を考えるべきだと感じました。今回で言えば、MySQLのTime型とDateTime型の違いについて調べればすぐに判明したのに、Rubyのことばかり調べて数時間を無駄にしてしまったからです。
これからも何度も壁にぶつかると思いますが、がんばって乗り越えて行こうと思います。