とーますメモ

Ruby on Rails / Goなどの学習メモ

【Rails】ActiveRecordの属性メソッドを上書きする

使用しているWebアプリの1つに
Railsが管理しているDBではない、他のDBからのデータを取得する必要があった。

当たり前の話だが、Railsではapplication.rbのtime_zoneの設定のため
日付取得の際に自動で、タイムゾーン変換が行われる。

thoames.hatenadiary.jp

そのため、以下の状況になってしまった。

①Railsが管理しているDBの日付は、自動タイムゾーン変換をしてほしい。
②Rails外のDBの日付は、自動タイムゾーン変換をしてほしくない。(*このDBの日付はDATETIME型)

前提として、このWebアプリではconfig.time_zoneは指定しているが、config.active_record.default_timezoneは指定していない。
その理由は以下の記事で詳しく解説されている。
Rails と時刻 - @kyanny's blog

この対応策としてタイトルにもある通り
Rails外のDBテーブルを管理するActiveRecordモデルの属性メソッドを上書きする」という方法で対応することにした。

その方法としては以下の記事が役に立った。

ActiveRecordのattribute methodをオーバーライドするとき
Active Recordで属性にアクセスするメソッドを上書きする - 飲んだり寝たり

読み込み専用の「read_attribute」のaliasである[]を利用することで、属性メソッド呼び出しの際に発生してしまうループ状況を回避し上書きすることができるようになる。
Railsの設定で「config.active_record.default_timezone」は設定していないのでRailsはDBの日付をUTCとして解釈し保存する。
これで元の日付データを読み込んだ後(この時点で日付は既にタイムゾーン変換が行われている)、UTCに戻してあげれば変換前の日付を取得できる。

UTCタイムゾーンの日付に変更する方法としては以下の方法があるが、それぞれ違いがある
1)to_s(:db)・・・返り値がString型 ex) self[:xxx_date].to_s(:db)
2)utc・・・返り値がTime型 ex) self[:xxx_date].utc
3)in_time_zone('UTC')・・・返り値がActiveSupport::TimeWithZone型 ex) self[:xxx_date].in_time_zone('UTC')

1)の方法を使用した場合、String型として返されるので、後ほど「strftime("%d/%m/%Y")」などを使用しフォーマットを変更したい場合などに
融通が効かないので不採用。問題は2)か3)なのだが、Time型とTimeWithZone型は互換性があるため、どちらにするか迷ったが
この記事(RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita)でも紹介されている通り、自分も極力TimeWithZone型を使用する方針で
行きたいのでここは3)を使用することにした。
※3)は内部で2)を行い、TimeWithZone型にラップしているっぽい。コードの詳細は以下。
rails/zones.rb at 98a57aa5f610bc66af31af409c72173cdeeb3c9e · rails/rails · GitHub


そして最終的に作成したコードは以下。このコードではxxx_dateがActiveRecordの属性メソッド。

class Hoge < ActiveRecord::Base

  establish_connection "xxxxxx".to_sym
  self.table_name = 'yyyyyy'

  def xxx_date
    self[:xxx_date].in_time_zone('UTC')
  end
end

長かった...

[参考]
TimeとTimeWithZoneの違いについて - 研鑽の日々
ruby - Rails 4 - in_time_zone unexpected behavior - Stack Overflow
When using time zones, beginning_of_day / end_of_day is broken in Rails 2 for any Date or DateTime - makandra dev