Jared Rader
Jared Rader

Reputation: 880

How can I set a default value for a date in Rails 5.2 with Postgres?

Using Rails 5.2 and Postgres 9.6.8

I have a date field that I want to store separately from created_at and updated_at because I don't care about the time, or even the day. I really just want to store the month and year.

I'm curious if there's a way to do this and give a default value.

In researching this question, I saw answers suggesting to do:

t.date :month_and_year, null: false, default: -> { 'NOW()' }

but this does not work. I event tried changing the column to a straight-up datetime but I never get a default value set.

I suppose I could store the month and year separately as integers, but I'm curious if anyone has come up with a good solution for this.

Upvotes: 1

Views: 2088

Answers (2)

Arup Rakshit
Arup Rakshit

Reputation: 118261

I agree with what mu is too short is said in his answer. But I would like to add here that there is a way to set it up in ActiveRecord by its new attribute API.

For example, if you have the column month_and_year inside a Post model and you want to set up a default for this, you can write that as below:

class Post < ApplicationRecord
  attribute :month_and_year, :date, default: -> { Date.today }
end

The above will set up a default date for you. See example:

Loading development environment (Rails 5.2.1)
2.5.1 :001 > post = Post.new(name: 'Post A', title: 'Post title', content: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.')
 => #<Post id: nil, name: "Post A", title: "Post title", content: "Lorem Ipsum is simply dummy text of the printing a...", created_at: nil, updated_at: nil, month_and_year: "2018-09-09">
2.5.1 :002 > post.save
   (0.2ms)  BEGIN
  Post Create (0.8ms)  INSERT INTO "posts" ("name", "title", "content", "created_at", "updated_at", "month_and_year") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["name", "Post A"], ["title", "Post title"], ["content", "Lorem Ipsum is simply dummy text of the printing and typesetting industry."], ["created_at", "2018-09-09 09:52:57.262764"], ["updated_at", "2018-09-09 09:52:57.262764"], ["month_and_year", "2018-09-09"]]
   (2.0ms)  COMMIT
 => true
2.5.1 :003 > Post.last
  Post Load (0.5ms)  SELECT  "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT $1  [["LIMIT", 1]]
 => #<Post id: 3, name: "Post A", title: "Post title", content: "Lorem Ipsum is simply dummy text of the printing a...", created_at: "2018-09-09 09:52:57", updated_at:"2018-09-09 09:52:57", month_and_year: "2018-09-09">
2.5.1 :004 >

Upvotes: 3

mu is too short
mu is too short

Reputation: 434615

The:default option ends up as part of the table's definition in the database. So this:

t.date :month_and_year, null: false, default: -> { 'NOW()' }

becomes SQL like this:

create table ... (
  ...
  month_and_year date not null default now(),
  ...
)

ActiveRecord will use default values from the table if it can understand them. If you have a simple default like this:

some_column integer default 6

then ActiveRecord will use that default value because it knows what 6 is and how to work with it.

In your case, the default value in the database is a call to the now() database function but AR doesn't know what now() means so it doesn't set a default value in the newly created model. When that model gets saved to the database, there will be no month_and_year so the database will use its default value for the month_and_year column in the new row; of course, ActiveRecord won't know anything about this so you have to call reload to get the month_and_year value out of the database.

How can you solve this? You could use an after_initialize hook like this:

after_initialize if: :new_record? do
  if(month_and_year.nil?)
    self.month_and_year = Date.today
  end
end

I do this sort of thing enough that I've whipped up a simple concern for it:

module RecordDefaults
  extend ActiveSupport::Concern

  module ClassMethods
    #
    # Examples:
    #
    #     use_defaults number: 6,
    #                  %i[time1 time2] => Time.method(:now),
    #                  array: [6, 11, 23, 42]
    #
    def use_defaults(defs)
      after_initialize if: :new_record? do
        defs.each do |attrs, value|
          if(value.respond_to?(:call))
            value = instance_exec(&value)
          else
            value = value.dup
          end
          attrs_without_values = ->(a) { send(a).nil? }
          use_the_default      = ->(a) { send("#{a}=", value) }
          Array(attrs).select(&attrs_without_values).each(&use_the_default)
        end
      end
    end
  end
end

and then:

class SomeModel < ApplicationRecord
  include RecordDefaults
  use_defaults month_and_year: Date.method(:today)
end

Upvotes: 2

Related Questions