Rails postgres Time attribute not saving - ruby-on-rails

Am I going insane? time attributes lose their value and reset to January 1 2000 after saving the active record object. The db is Postgres. Rails 5.2 and pg gem 1.1.3
2.6.6 :019 > obj.end_time = Date.new(2019, 9,9)
=> Mon, 09 Sep 2019
2.6.6 :020 > obj.save
=> true
2.6.6 :021 > obj.end_time
=> Mon, 09 Sep 2019 00:00:00 UTC +00:00
2.6.6 :022 > obj.end_time.class
=> ActiveSupport::TimeWithZone
But now if I reload from the db, the value is reset:
2.6.6 :023 > obj.reload
2.6.6 :024 > obj.end_time
=> Sat, 01 Jan 2000 00:00:00 UTC +00:00
2.6.6 :025 > obj.end_time.class
=> ActiveSupport::TimeWithZone
The postgres column type is shown in psql as "time without time zone". I guess I just need to specify datetime instead of time in migrations if I want to set the date part?
I tried to change column type in a migration change_column :school_courses, :start_time, :datetime but got:
PG::DatatypeMismatch: ERROR: column "start_time" cannot be cast
automatically to type timestamp without time zone HINT: You might
need to specify "USING start_time::timestamp without time zone"
Not sure how to do this

Use datetime column type to hold a date and time
Only use time in the migration if you don't need the date (only want to store time part)
This is the case with postgres anyway
Migration example:
create_table "school_years" do |t|
t.integer "year"
t.integer "minimum_candidates_per_exam"
t.time "am_start_time"
t.time "pm_start_time"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Although I generally just use datetime and set the year part as well, and avoid using time

Related

Associate Active Record tables through non-primary, non-unique column in Rails

I have a table "films" whose schema looks like this:
create_table "films", force: :cascade do |t|
t.integer "tmdb_id", null: false
t.string "language_iso_639_1", null: false
t.string "title", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["tmdb_id", "language_iso_639_1"], name: "index_films_on_tmdb_id_and_language_iso_639_1", unique: true
end
As you can see the tmdb_id itself is not unique, only the combination of the tmdb_id and the language_iso_639_1 together.
So far this is working as expected and lets me create records like these:
[#<Film:0x0000000108156630
id: 1,
tmdb_id: 12,
language_iso_639_1: "en",
title: "Finding Nemo",
created_at: Fri, 15 Apr 2022 14:58:43.128785000 UTC +00:00,
updated_at: Fri, 15 Apr 2022 14:58:43.128785000 UTC +00:00>,
#<Film:0x00000001081564f0
id: 2,
tmdb_id: 12,
language_iso_639_1: "de",
title: "Findet Nemo",
created_at: Fri, 15 Apr 2022 14:58:52.563142000 UTC +00:00,
updated_at: Fri, 15 Apr 2022 14:58:52.563142000 UTC +00:00>,
#<Film:0x0000000108156428
id: 3,
tmdb_id: 12,
language_iso_639_1: "fr",
title: "Le Monde de Nemo",
created_at: Fri, 15 Apr 2022 14:59:03.318667000 UTC +00:00,
updated_at: Fri, 15 Apr 2022 14:59:03.318667000 UTC +00:00>]
Now I want to create another table "film_backdrops" to save backdrop-images for the specific films. Those images do not differ for different languages, so only the tmdb_id is important.
My (probably to be revised) scheme for this table currently looks like this:
create_table "film_backdrops", force: :cascade do |t|
t.integer "tmdb_id", null: false
t.string "file_path", null: false
t.float "vote_average"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
And an entry would look like this, for example:
#<FilmBackdrop:0x0000000108a3e798
id: 1,
tmdb_id: 12,
file_path: "/bla/bla/image.png",
vote_average: 9.8,
created_at: Fri, 15 Apr 2022 15:00:59.635324000 UTC +00:00,
updated_at: Fri, 15 Apr 2022 15:00:59.635324000 UTC +00:00>
I want to link these two tables so that the same FilmBackdrop records are returned for films with the same tmdb_id and irrespective of the language_iso_639_1 field, like this:
Film.find_by(tmdb_id: 12, language_iso_639_1: "en").film_backdrops # => #<FilmBackdrop:0x0000000107b445d0
Film.find(tmdb_id: 12, language_iso_639_1: "de").film_backdrops # => #<FilmBackdrop:0x0000000107b445d0
Film.find(tmdb_id: 12, language_iso_639_1: "fr").film_backdrops # => #<FilmBackdrop:0x0000000107b445d0
The other way around should also work:
FilmBackdrop.find(1).films # => #<Film:0x00000001077cd348, #<Film:0x00000001077cd280 , #<Film:0x00000001077cd1b8
Since I don't need the primary key "id" of Film for this and "tmdb_id" of Film alone is not unique either, no approach I have tried so far has been successful.
Is it even possible to get it done without creating a third "inbetween" table?
It sounds like you're missing a model in which the value of tmdb_id is a unique key.
This would have a has_many association to Film, and, I think, has_many to FilmBackdrop.
Then Film and FilmBackdrop would each has_many of the other through this model.
Assuming no changes to your provided schema, the only thing you need to do is specify the foreign_key and primary_key options on your associations:
class Film
has_many :film_backdrops, foreign_key: :tmdb_id, primary_key: :tmdb_id
end
class FilmBackdrop
has_many :films, foreign_key: :tmdb_id, primary_key: :tmdb_id
end
Note that while this will work for your purposes, it's non-standard in Rails. So some functionality, like the inverse associations, will not be supported.
What I'm guess that your doing here is localizing films and this solution feels very backwards and overcomplicated.
Rails doesn't really do composite primary keys / foreign keys as thats not something thats widely supported among databases. There is a gem for composite primary keys but it is built on hacking ActiveRecord internals so I would consider if this is something you really want to inflict on your app.
I would instead do it like so:
class Film
has_many :film_localizations
has_many :film_backdrops
end
# rails g model film_localization film:belongs_to language_iso_639_1:string title:string
class FilmLocalization
belongs_to :film
end
If you want to use an externally derived UUID instead of an auto-incrementing integer or generated UUID for the films table thats perfectly fine.
But your models should have a single unique primary key which other tables can use to refer to it or you're going to have a very difficult time as you're fighting the entire ORM / conventions.
Delegation can be used if you want a FilmLocalization to behave like a Film.
There are also ready made solutions for translating model data such as Mobility.

Why does an ActiveRecord object return DateTime.now differently than DateTime.now

Say I have a simple ActiveRecord model with this example schema:
create_table "users", id: :serial do |t|
t.datetime "updated_at"
end
When I run the following code in the console:
user = User.new
user.updated_at = DateTime.now
user.updated_at # => Thu, 27 Feb 2020 09:28:51 GMT +00:00
My question is: why this output is different than the normal return value of DateTime.now, which is
DateTime.now # => Thu, 27 Feb 2020 10:28:51 +0100
Is there some kind of serializer involved? How can I find out more about this?
AR updated_at/created_at are time zone aware attributes. You can read more about them here:
https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
If you grab a User record for instance, and call time_zone_aware_types on it, you'll get the timezone aware column types
user = User.first
user.time_zone_aware_types
=> [:datetime, :time]
These types of columns are automatically converted to Time.zone when retrieved from the database even though they are stored in UTC.
The difference with DateTime.now is that it's not timezone aware and just returns the time in UTC
add your time zone in your config/application.rb inside the
class Application < Rails::Application
.......
# it will be like this
config.time_zone = "Asia/Riyadh"
.....
end

Ruby on Rails time zone strange behaviour

There is task model with attributes when and duration.
create_table "tasks", force: true do |t|
...
t.datetime "when"
t.integer "duration"
...
end
I wrote method for checking if task is active so I can show it on page.
This is active method:
def active?
if (self.when + self.duration) > Time.now
true
end
end
I tried in console to inspect object:
t.when + t.duration
=> Sun, 08 Sep 2013 01:01:00 UTC +00:00
DateTime.now
=> Sun, 08 Sep 2013 01:57:13 +0200
t.active?
=> true
It's true but I entered 1:00 time and 1 minute for duration and I hoped it shouldn't be true.
It seems that when column in database is not saved in correct time zone, so it gives incorrect results. How to solve this issue?
It seems that when column in database is not saved in correct time zone
1) Rails automatically converts times to UTC time before inserting them in the database (which is a good thing), which means the times have an offset of "+0000" . That means if you save a time of 8pm to the database, and your server is located in a timezone with an offset of "+0600", then the equivalent UTC time is 2pm, so 2pm gets saved in the database. In other words, your local server's time is 6 hours ahead of UTC time, which means that when it's 8pm in your server's time zone, it's 2pm in the UTC timezone.
2) When you compare dates, ruby takes the timezone offset into account--in other words ruby converts all times to the same timezone and then compares the times. Here is an example:
2.0.0p247 :086 > x = DateTime.strptime('28-01-2013 08:00:00 PM +6', '%d-%m-%Y %I:%M:%S %p %z')
=> Mon, 28 Jan 2013 20:00:00 +0600
2.0.0p247 :087 > y = DateTime.strptime('28-01-2013 08:20:00 PM +7', '%d-%m-%Y %I:%M:%S %p %z')
=> Mon, 28 Jan 2013 20:20:00 +0700
2.0.0p247 :088 > x < y
=> false
If you just compare the times of the two Datetime objects, x is less than y. However, y has a time of 8:20pm in a timezone that has an offset of +7, which is equivalent to the time 7:20pm in a timezone with an offset of +6. Therefore, y is actually less than x. You need to compare apples to apples, which means you need to mentally compare times that have been converted to the same timezone to get the same results as ruby/rails produces.
3) You can convert Time.now to a UTC time using the rails utc() method:
2.0.0p247 :089 > x = Time.now
=> 2013-09-07 8:00:00 +0600
2.0.0p247 :090 > x.utc
=> 2013-09-07 02:00:00 UTC
That's what ruby does before comparing Time.now to task.when + task.duration
4) You might find it more convenient to create a DateTime object with the time you want using:
DateTime.strptime('28-01-2013 08:00:00 PM +0', '%d-%m-%Y %I:%M:%S %p %z'
Because you are able to specify the offset as zero, you don't have to create a time that anticipates the conversion to UTC time.
Or you can use the change() method, which causes the offset() to change without converting the time:
2.0.0p247 :011 > x = DateTime.now
=> Sun, 08 Sep 2013 00:34:08 +0600
2.0.0p247 :012 > x.change offset: "+0000"
=> Sun, 08 Sep 2013 00:34:08 +0000
ActiveRecord stores timestamps in UTC by default. See How to change default timezone for Active Record in Rails? for changing default time zone.
You can also just use Time#in_time_zone to convert t.when to your timezone, see http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html.

Time fields in Rails coming back blank

I have a simple Rails 3.b1 (Ruby 1.9.1) application running on Sqlite3. I have this table:
create_table :time_tests do |t|
t.time :time
end
And I see this behavior:
irb(main):001:0> tt = TimeTest.new
=> #<TimeTest id: nil, time: nil>
irb(main):002:0> tt.time = Time.zone.now
=> Mon, 03 May 2010 20:13:21 UTC +00:00
irb(main):003:0> tt.save
=> true
irb(main):004:0> TimeTest.find(:first)
=> #<TimeTest id: 1, time: "2000-01-01 20:13:21">
So, the time is coming back blank. Checking the table, the data looks OK:
sqlite> select * from time_tests;
1|2010-05-03 20:13:21.774741
I guess it's on the retrieval part? What's going on here?
Technically, it's not coming back blank. It comes back as a time with a default date. It returns 2000-01-01 20:13:21 as the time, which is expected.
Rails is doing some magic loading of the data into a Time object and clearing out the date (since you only told it to store the time).
If you want to store the date and time then you need to define the column as a datetime. And conversely, if you wanted just a date you would use date.
So to recap:
date => "2010-12-12 00:00:00"
time => "2000-01-01 13:14:15"
datetime => "2010-12-12 13:14:15"

Comparing dates in rails

Suppose I have a standard Post.first.created_at datetime. Can I compare that directly with a datetime in the format 2009-06-03 16:57:45.608000 -04:00 by doing something like:
Post.first.created_at > Time.parse("2009-06-03 16:57:45.608000 -04:00")
Edit: Both fields are datetimes, not dates.
Yes, you can use comparison operators to compare dates e.g.:
irb(main):018:0> yesterday = Date.new(2009,6,13)
=> #<Date: 4909991/2,0,2299161>
irb(main):019:0> Date.today > yesterday
=> true
But are you trying to compare a date to a datetime?
If that's the case, you'll want to convert the datetime to a date then do the comparison.
I hope this helps.
Yes you can compare directly the value of a created_at ActiveRecord date/time field with a regular DateTime object (like the one you can obtain parsing the string you have).
In a project i have a Value object that has a created_at datetime object:
imac:trunk luca$ script/console
Loading development environment (Rails 2.3.2)
>> Value.first.created_at
=> Fri, 12 Jun 2009 08:00:45 CEST 02:00
>> Time.parse("2009-06-03 16:57:45.608000 -04:00")
=> Wed Jun 03 22:57:45 0200 2009
>> Value.first.created_at > Time.parse("2009-06-03 16:57:45.608000 -04:00")
=> true
The created_at field is defined as:
create_table "values", :force => true do |t|
[...]
t.datetime "created_at"
end
N.B. if your field is a date and not a datetime, then you need to convert it to a time:
Post.first.created_at.to_time > Time.parse("2009-06-03 16:57:45.608000 -04:00")
or parse a date:
Post.first.created_at > Date.parse("2009-06-03 16:57:45.608000 -04:00")
otherwise you'll get a:
ArgumentError: comparison of Date with Time failed

Resources