Reputation: 1398
I am upgrading a Rails app from
in various steps. Since the Rails upgrade requires the Postgres upgrade I can't separate the upgrades in a sensible way.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone
, even if the database column has no time zone. Previously it was a plain Time
object which had a different default JSON representation.
This makes a lot of API tests fail which were previously all returning UTC times.
Example for Rails 4.2, Ruby 2.2, PG 9.1 for a PhoneNumber
object:
2.2.6 :002 > p.time_weekdays_from
=> 2000-01-01 07:00:00 UTC
2.2.6 :003 > p.time_weekdays_from.class
=> Time
Example for Rails 5.2, Ruby 2.5, PG 10:
irb(main):016:0> p.time_weekdays_from
=> Sat, 01 Jan 2000 11:15:00 CET +01:00
irb(main):018:0> p.time_weekdays_from.class
=> ActiveSupport::TimeWithZone
I have added an initializer to override this for the time being and this seems to work fine, but I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
# This works, but why is it necessary?
module ActiveSupport
class TimeWithZone
def as_json(options = nil)
self.utc.iso8601
end
end
end
PS: I don't always want UTC, I just want it for the API because that's what our API clients expect.
Upvotes: 4
Views: 1272
Reputation: 164829
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
This change was made because Ruby's default Time
has no understanding of time zones. ActiveSupport::TimeWithZone
can. This solves a lot of problems when working with times, time zones, and databases.
For example, let's say your application's time zone is America/Chicago
. Previously you had to decide whether you're going to store your times with or without time zones. If you opt for without a time zone, do you store it as UTC or as America/Chicago? If you store it as UTC, do you convert it to America/New York on load or on display? Conversion means adding and subtracting hours from the Time
. When you save Time
objects you have to be careful to remember what time zone the Time
was converted to and to convert it back to the database's time zone. Coordinating all this leads to many bugs.
Rails 5 introduces ActiveSupport::TimeWithZone
. This stores the time as UTC and the desired time zone to represent it in. Now handling time is simpler: store it as UTC (ie. timestamp
) and add the application's time zone on load. No conversion is necessary. Rails handles this for you.
The change is now timestamp
columns, by default, will be formatted in the application's time zone. This takes some getting used to, but ultimately will make your handling of times and time zones more robust.
> Time.zone.tzinfo.name
=> "America/Chicago"
> Time.zone.utc_offset
=> -21600
# Displayed in the application time zone
> Foo.last.created_at
=> Tue, 31 Dec 2019 17:16:14 CST -06:00
# Stored as UTC
> Foo.last.created_at.utc
=> 2019-12-31 23:16:14 UTC
If you have code which manually does time zone conversions, get rid of it. Work in UTC. Time zones are now just formatting.
As much as possible...
def get_api_time
'2000-01-01 07:00:00 UTC'
end
# bad: downgrading to strings, implicit formatting
expected_time = Time.utc(2000, 1, 1, 7)
expect( get_api_time ).to eq expected_time
# good: upgrading to objects, format is irrelevant
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect(
Time.zone.parse(get_api_time)
).to eq expected_time
# better: refactor method to return ActiveSupport::TimeWithZone
def get_api_time
Time.zone.parse('2000-01-01 07:00:00 UTC')
end
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect( get_api_time ).to eq expected_time
I recommend reading these articles, they clear things up.
Upvotes: 2