要約
Rails(ActiveRecord) で timestamp
に不用意に null: false
を使うのは止めましょう。痛い目を見ます。
前提
Rails で社内の業務システムを作っていて、そのうちの一つに処理を予約できる機能があります。モデルはこんな感じです。
scheduled_task.rb
class ScheduledTask < ApplicationRecord
include ScheduledTasksHelper
validates :mail, :task, :done_at, presence: true
(省略)
end
そして migration は(2段階ですが)こんな感じでした。
class CreateScheduledTasks < ActiveRecord::Migration[4.2]
def change
create_table :scheduled_tasks, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8', force: :cascade do |t|
t.string :mail
t.string :task
t.timestamp :done_at
t.timestamps null: false
end
end
end
...
class NotNullToScheduledTasks < ActiveRecord::Migration[4.2]
def change
change_column :scheduled_tasks, :mail, :string, :null => false
change_column :scheduled_tasks, :task, :string, :null => false
change_column :scheduled_tasks, :done_at, :timestamp, :null => false
end
end
※ このときは Rails 4.2 系でした。今は 5.2 系です。
起きたこと & 解決方法
問題発生
上の状態だと db/schema.rb
がこんな風になります。
db/schema.rb
create_table "scheduled_tasks", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "mail", null: false
t.string "task", null: false
t.timestamp "done_at", default: -> { "current_timestamp()" }, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
これで何が起きたかというと...
task = ScheduledTask.new(mail: 'foo@example.jp', task: 'lock:yes', done_at: Time.now + 7200)
task.save!
puts task.done_at #=> 2019-04-07 23:54:05 +0900
これは問題ないのですが
task.update_attributes!(mail: 'bar@example.jp')
puts task.done_at #=> 2019-04-07 21:54:10 +0900
あれ…? 設定した日時が変更されている!? なぜ? 変更していないのに…???
しばらく悩んだ後、答えは目の前に書いてありました。
db/schema.rb
(省略)
t.timestamp "done_at", default: -> { "current_timestamp()" }, null: false
「なんか default: -> { "current_timestamp()" }
って書いてあるんですけど…」
解決
原因が分かれば、何故こうなったかも分かりました。
-
default
にcurrent_timestamp()
が設定されている - 新規登録(
ScheduledTask.create
)の際はdone_at
が必須だが、更新(ScheduledTask.save
)の際は必須ではない-
done_at
の値が変わらない場合、ActiveRecord が更新 SQL 文からdone_at
を省略する -
done_at
が省略された場合、current_timestamp()
によって現在日時が設定される
-
ということで以下の migration を適用し、
20190407074931_remove_default_from_done_at.rb
class RemoveDefaultFromDoneAt < ActiveRecord::Migration[5.2]
def change
change_column :scheduled_tasks, :done_at, :datetime, null: false
end
end
以下のように done_at
からデフォルト値を削除することで想定通りの動きになりました。
db/schema.rb
create_table "scheduled_tasks", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.string "mail", null: false
t.string "task", null: false
t.datetime "done_at", null: false
(省略)
end
疑問
しかし何処で current_timestamp()
がデフォルトに設定されたんだろうか? おかしいなぁ…