5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ActiveRecord で時間の長さを保存する

Last updated at Posted at 2018-07-04

1.year2.weeks などで生成される ActiveSupport::Duration をデータベースに保存してロードできるようにしたい。

どうシリアライズする?

ActiveSupport::Duration をそのまま DB に保存はできないので、シリアライズの方法を考える必要がある。

秒数

ActiveSupport::Duration には to_i メソッドがあり、秒数を返す。これは必ずしも実際の秒数ではない (たとえば月によって日数がかわるため) が、ActiveSupport::Duration.build に渡せばだいたい復元することができる。

値をみてもどれくらいの期間なのかはわかりにくいが、たんなる整数なので、DB に不正な値が入る危険が少なく、比較やソートもしやすい。

しかし、たとえば 1.month.to_i2629746 と定義されており 2629746.seconds.to_i と区別できない。ActiveSupport::Duration.build(2629746)1.month と解釈されるので、だいたいの場合は問題ないだろうが、やや厳密さにかける。

※ 年の秒数はグレゴリオ年の秒数、月の秒数はその 1/12 と定義されている

ISO8601

もう一つの方法として、iso8601 メソッドで得られる ISO8601 の継続時間を表す文字列が使える。たとえば 2.years.iso8601P2Y10.minutes.iso8601PT10M のようになる。これは ActiveSupport::Duration.parse で復元できる。

読み方さえわかれば秒数よりも理解しやすいが、比較やソートは難しい。

この方法だと 1.month.iso8601P1M2629746.seconds.iso8601PT2629746S のように区別され、ActiveSupport::Duration.parse に渡すとそれぞれもとの形に復元される。

※ただし 0.year.iso8601 0.day.iso8601 0.hour.iso8601 などは区別されず PT0S になるようだ

なお、ISO8601 には 14.daysP0000-00-14T00:00:00 のように表現する形式もあるが、この形式では 30 時間 のような値を表現できないため、これ以上検討しない。

ActiveRecord::Type::Value

上記の ISO8601 による文字列表現を使って、ActiveRecord::Attributes::ClassMethods#attribute で使える型を定義する。

# DB での型に対応するクラスを継承
class DurationType < ActiveRecord::Type::String
  # attribute_name= で assign された値の変換
  def cast(value)
    case value
    when ActiveSupport::Duration
      value
    when NilClass
      nil
    else
      begin
        ActiveSupport::Duration.parse(value.to_s)
      rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
        nil
      end
    end
  end

  # DB から読んだ値の変換
  def deserialize(value)
    return nil if value.nil?
    ActiveSupport::Duration.parse(value)
  end

  # DB に書き込む値
  def serialize(value)
    return nil if value.nil?
    value.iso8601
  end
end

ActiveRecord::Type.register(:duration, DurationType)
# attribute :attribute_name, :duration

これで下記のように使える。

class Plan < ActiveRecord::Base
  attribute :term, :duration
end

Plan.create!(term: 2.years).reload.term == 2.years # true
Plan.create!(term: "PT3H").reload.term == 3.hours # true
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?