Rails 5.2 and Active Record migration with CURRENT_TIMESTAMP
Asked Answered
L

1

5

I have some attributes that need to have default values. I've set up my migrations to set the defaults in the database as follows:

class AddDefaultsToModel < ActiveRecord::Migration[5.2]
  def change
    change_column :posts, :post_type, :string, default: 'draft'
    change_column :posts, :date_time, :datetime, default: -> { 'CURRENT_TIMESTAMP' }
  end
end

The defaults work great when adding directly to the database. However, if I build a new model in Rails, one attribute works as expected, the other doesn't:

post = Post.new
post.post_type # draft (as expected)
post.date_time # nil (expecting the current date and time)

Is this behavior deliberate? Do I have to set the default in the model as well? Why does Post#post_type work but not Post#date_time?

Lungfish answered 16/12, 2018 at 17:34 Comment(0)
A
4

ActiveRecord doesn't understand what your default value for date_time means so it doesn't give date_time a default value at all. Then, when you insert the row into the database (i.e. post.save), the database will use the current timestamp as the date_time value (presuming of course that no one has touched date_time). Rails won't know that date_time has a value after the insert so you get behavior like this:

post = Post.new
post.date_time # `nil` because it hasn't been set at all
# set some other values on `post`...
post.save      # INSERT INTO posts (columns other than date_time...) values (...)
post.date_time # `nil` even thought there is a value in the database
post.reload    # Pull everything out of the database
post.date_time # Now we have a timestamp

You have some options:

  1. Call post.reload after saving the post to get the default timestamp that the database used.

  2. Use an after_initialize hook to set the default yourself:

    class Post < ApplicationRecord
      after_initialize if: :new_record? do
        self.date_time = Time.now
      end
    end
    
  3. Use the attributes API to set the default by hand:

    class Post < ApplicationRecord
      attribute :date_time, :datetime, default: ->{ Time.now }
    end
    

    You need to use a lambda (or Proc) so that Time.now is executed at the right time.

Aubrey answered 16/12, 2018 at 19:46 Comment(2)
I've been using the attributes API but it feels like a bug if I have to define the default twice so I had to ask the question in case I was missing something.Lungfish
I don't think you're missing anything, ActiveRecord has a limited understanding of the database so it has now idea what default current_timestamp or default now() means so you have to do it yourself. And you have to do it twice if you want to be able to insert records without going through Rails.Aubrey

© 2022 - 2024 — McMap. All rights reserved.