はじめに
Rails で日付の範囲指定をしてレコードを絞り込むことがあると思います。
範囲の両端が決まっているときは簡単なのですが、片方の端が決まっていない場合にどうするのか、無限大を使う方法があるとのことだったのですが、ちょっと調べないとわからなかったので、備忘としてまとめます。
環境情報
- PostgreSQL 10.5
- Rails 5.2.2
- ruby 2.4.2
前提
記事を表す Post
モデルに、published_at
のような公開日カラムがあるとします。
ある期間に公開された記事を検索する場合
例えば 2019/05/01 ~ 2019/06/01 の間に公開された記事を検索する場合は、以下のように書くことができると思います。
Post.where(published_at: Time.local(2019,5,1)..Time.local(2019,6,1))
SQL は BETWEEN
を使う形になっています。
irb(main):019:0> Post.where(published_at: Time.local(2019,5,1)..Time.local(2019,6,1)).to_sql
=> "SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"published_at\" BETWEEN '2019-04-30 15:00:00' AND '2019-05-31 15:00:00'"
ある期間を越えている記事のみ検索する場合
すぐわからなかったのが、published_at >= 2019/05/01
のような、ある期間を越えている記事のみ検索したいという場合についてでした。
結果としては、以下のような形で可能でした。
Post.where(published_at: Time.local(2019,5,1)..Float::INFINITY)
端を指定しない側に、浮動小数点数の無限大を表す Float::INFINITY
を指定する方法です。
ruby の Range
クラスにおいて、無限遠を指定する際には Float::INFINITY
を指定できますが、
Time
クラス、Date
クラス、DateTime
クラスでも使えるようでした。
SQL は以下のように、不等号の形となっています。
irb(main):018:0> Post.where(published_at: Time.local(2019,5,1)..Float::INFINITY).to_sql │
=> "SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"published_at\" >= '2019-04-30 15:00:00'"
利用にあたってハマったところ
上記で片端のみ指定する形で日付の範囲指定ができるようになったのですが、
利用にあたってハマったところがあったので、そちらもまとめます。
負の無限大で範囲指定できない
上記の例では以下のように、正の無限大 Float::INFINITY
を指定し、「ある日付を越えた日付を持つレコード」を表したのですが、
逆に「ある日付より以前の日付を持つレコード」を検索したい場合もあると思います。
そんなとき、以下のように書きたい気持ちになるのですが、これを実行すると ArgumentError: bad value for range
というエラーが発生します。
Post.where(published_at: -::Float::INFINITY..Time.local(2019,5,1))
こちらは ruby のバグとして報告されているようで、まだ解決していないようでした。。
そのため以下のように not
を使い、あくまで正の無限大を範囲に指定してあげる必要がありました。
Post.where.not(published_at: Time.local(2019,5,1)..Float::INFINITY)
null になっている値を検索できない
上記のように範囲検索を使う形にすると、日付のカラム値が null
になっているレコードが検索対象外になってしまいます。
そのため、マイグレーション作成時に null: false
としてあげるのが良さそうですが、
ではデフォルト値はどうするんだ?ということで悩みました。
postgresql であれば、文字列として infinity
と指定してあげることで、
日付における無限大が表せるようで、rails 側で範囲検索の対象に含めることができるようになりました。
例ですが、マイグレーションはこんな感じになるかと思います。
class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :title
t.datetime :published_at, null: false, default: 'infinity'
t.timestamps
end
end
end
以下コミットで、date
、datetime
、timestamp
型にも対応されているようです。
おわりに
利用にあたっていろいろ気をつけないといけないことが多かったですが、使えるようになると実装の幅が広がるのでは、と思いました。
これから利用する機会があれば使ってみようと思います。