目的
月次の集計結果や、職歴の開始年月など、日付が意味を持たず年月のみだけを管理したいケースがある。
そういった場合にどう実装するか。
方針
- DB上は、DATE型で値を保持する
- 日付は必ず月初日(1日)にして意味を持たせない
文字列等他の形式で保持する方法もあるが、DATE型にするとデータの整合性を守りやすく、ソートや検索時の利便性も高い。
問題点
年月を画面から入力する際、html の input 要素には month 型がある。
<input type="month"> - HTML | MDN
Rails の form にも month_field が用意されている。
しかし、 month 型はパラメータの値が YYYY-MM 形式の文字列であるため、どこかで日付型に変換する必要がある。
解決策
ActiveRecord::Type::Date を拡張して年月用の Type を用意し、 YYYY-MM の文字列を解釈できるようにする。
実装例
年月型。
配置場所は app/models/types 配下にしたが、もっと良さげな場所があるかもしれない。
module Types class YearMonth < ActiveRecord::Type::Date def cast_value(value) date = if value.is_a?(String) && value.match?(%r{^\d{4}-(?:0[1-9]|1[0-2])$}) # YYYY-MM 形式の文字列の場合は日付を追加したうえで日付に変換する Time.zone.parse("#{value}-01").to_date else # それ以外の変換は親クラスに任せる super end # 日付型に変換できた場合は月初日にする date&.beginning_of_month end def changed_in_place?(raw_old_value, new_value) # 年月が同じであれば値が変わっていないとみなすよう、cast してから比較するようにしておく cast(raw_old_value) != cast(new_value) end end end
initializer で作成した年月型を読み込む。
# app/config/initializers/types.rb Rails.application.reloader.to_prepare do ActiveRecord::Type.register(:year_month, Types::YearMonth) end
モデル側では attribute で年月型であることを指定する。
class SampleModel < ApplicationRecord attribute :sample_month, :year_month end