简体   繁体   中英

Rails 5 and PostgreSQL 'Interval' data type

Does Rails really not properly support PostgreSQL's interval data type?

I had to use this Stack Overflow answer from 2013 to create an interval column, and now it looks like I'll need to use this piece of code from 2013 to get ActiveRecord to treat the interval as something other than a string.

Is that how it is? Am I better off just using an integer data type to represent the number of minutes instead?

From Rails 5.1, you can use postgres 'Interval' Data Type, so you can do things like this in a migration:

add_column :your_table, :new_column, :interval, default: "2 weeks"

Although ActiveRecord only treat interval as string, but if you set the IntervalStyle to iso_8601 in your postgresql database, it will display the interval in iso8601 style: 2 weeks => P14D

execute "ALTER DATABASE your_database SET IntervalStyle = 'iso_8601'"

You can then directly parse the column to a ActiveSupport::Duration

In your model.rb

def new_column
  ActiveSupport::Duration.parse self[:new_column]
end

More infomation of ISO8601 intervals can be find at https://en.wikipedia.org/wiki/ISO_8601#Time_intervals

I had a similar issue and went with defining reader method for the particular column on the ActiveRecord model. Like this:

class DivingResults < ActiveRecord::Base
  # This overrides the same method for db column, generated by rails
  def underwater_duration
    interval_from_db = super
    time_parts = interval_from_db.split(':')
    if time_parts.size > 1 # Handle formats like 17:04:41.478432
      units = %i(hours minutes seconds)
      in_seconds = time_parts
        .map.with_index { |t,i| t.to_i.public_send(units[i]) }
        .reduce(&:+) # Turn each part to seconds and then sum
      ActiveSupport::Duration.build in_seconds
    else # Handle formats in seconds
      ActiveSupport::Duration.build(interval_from_db.to_i)
    end
  end
end

This allows to use ActiveSupport::Duration instance elsewhere. Hopefully Rails will start handling the PostgreSQL interval data type automatically in near future.

A more complete and integrated solution is available in Rails 6.1


The current answers suggest overriding readers and writers in the models. I took the alter database suggestion and built a gem for ISO8601 intervals, ar_interval .

It provides a simple ActiveRecord::Type that deals with the serialization and casting of ISO8601 strings for you!

The tests include examples for how to use it .

If there is interest, the additional formats Sam Soffes demonstrates could be included in the tests

Similar to Madis' solution, this one handles fractions of a second and ISO8601 durations:

def duration
  return nil unless (value = super)

  # Handle ISO8601 duration
  return ActiveSupport::Duration.parse(value) if value.start_with?('P')

  time_parts = value.split(':')
  if time_parts.size > 1
    # Handle formats like 17:04:41.478432
    units = %i[hours minutes seconds]
    in_seconds = time_parts.map.with_index { |t, i| t.to_f.public_send(units[i]) }.reduce(&:+)
    ActiveSupport::Duration.build in_seconds
  else
    # Handle formats in seconds
    ActiveSupport::Duration.build(value)
  end
end

def duration=(value)
  unless value.is_a?(String)
    value = ActiveSupport::Duration.build(value).iso8601
  end

  self[:duration] = value
end

This assumes you setup your database like Leo mentions in his answer. No idea why sometimes they come back from Postgres in the PT42S format and sometimes in the 00:00:42.000 format :/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM