Reputation: 880
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
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
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