LoginSignup
2
3

More than 5 years have passed since last update.

Rails で不用意に timestamp に null: false してはいけない

Posted at

要約

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()" } って書いてあるんですけど…」

解決

原因が分かれば、何故こうなったかも分かりました。

  • defaultcurrent_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() がデフォルトに設定されたんだろうか? おかしいなぁ…

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3