Simple minds think alike

より多くの可能性を

【Rails】範囲オブジェクト(Range)を使ったActiveRecordのwhere比較、範囲検索のコードの書き方

ActiveRecordのwhereを使って、色々な書き方で比較演算(>, >=, <, <=)・範囲抽出(Beetween、◯以上□未満)を実装できますが、最近では範囲オブジェクト (Range (例: 10..30)) を使ってほとんど実装することができるようになっています。

ただ、範囲オブジェクトを使った実装は

  • Ruby2.7から導入されたbeginless range
    • 範囲オブジェクトの開始の値を省略できる書き方(例: ..30)
  • Ruby 2.6から導入されたendless range
    • 範囲オブジェクトの終わりの値を省略できる書き方(例: 10..)

を使うと綺麗に書けるので、可能であれば対応したRuby, Railsバージョンにすると良いと思います。

範囲オブジェクトを使った書き方のメリット

範囲オブジェクトを使った書き方

個人的には範囲オブジェクトを使った書き方が

  • コード量が少なく可読性が高い(コードを読んで挙動が分かりやすい)
    • 保守面でのメリットが高い
  • エラーになりにくい
    • 開発面でのメリットが高い

ので、一番良いと思っています。具体的には、

# Rails(ActiveRecord)
User.where(age: ...30)

という書き方で

-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" < 30"

というSQLクエリが発行されます。

以下のような書き方で以前から範囲オブジェクトを使って実現できましたが、開始の値に意味の無い値を書く必要があったので、使っていませんでした。

# Rails(ActiveRecord)
User.where(age: - Float::INFINITY...30)

範囲オブジェクトを使わない書き方

範囲オブジェクトが使うようになるまでは、よく以下のような書き方をしていました。

# Rails(ActiveRecord)
User.where('age < ?', 30)

無駄がない書き方ではあるのですが、テーブルを結合して両方のテーブルにある同じ名前のカラムを抽出条件にする場合、エラーになるというデメリットがありました。

User.joins(:family_members).where('age < ? ', 30)
=> SELECT "users".* FROM "users" INNER JOIN "family_members" ON 
    "family_members"."user_id" = "users"."id" 
   WHERE (age < 30)

PG::AmbiguousColumn: ERROR:  column reference "age" is ambiguous

範囲オブジェクトを使った書き方では、この問題が解決されています。

User.joins(:family_members).where(age: ...30)
=> SELECT "users".* FROM "users" INNER JOIN "family_members" ON 
    "family_members"."user_id" = "users"."id" 
   WHERE "family_members"."age" < 30
User.joins(:family_members).where(family_members: { age: ...30 })
=> SELECT "users".* FROM "users" INNER JOIN 
    "family_members" ON "family_members"."user_id" = "users"."id" 
   WHERE "users"."age" < 30

範囲オブジェクトを使った書き方の注意点

ケース別の書き方

それでは、比較機能(>, >=, <, <=)・範囲抽出(Beetween)それぞれのケース別の書き方をご紹介します。以下の例は抽出条件が、整数のみですが、時間や日付型のカラムでも大丈夫です。

◯◯より小さい (less than <)

# Rails(ActiveRecord)
User.where(age: ...30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" < 30"

◯◯以下 (less than or equal <=)

# Rails(ActiveRecord)
User.where(age: ..30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" <= 30"

◯◯以上 (greater than or equal >=)

# Rails(ActiveRecord)
User.where(age: 11..)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" >= 11"

◯◯よりも大きい (greater than >)

範囲オブジェクトを使った書き方では、今のところ以下のSQLクエリは発行できません。

SELECT "users".* FROM "users" WHERE "users"."age" > 10"

なので、範囲オブジェクトを使う場合は◯◯以上 (greater than or equal >=)というSQLクエリに置き換える必要があります。ですが、あまり困ることもなさそうです。

範囲抽出(Beetween)

今まで通り

# Rails(ActiveRecord)
User.where(age: 10..30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" BETWEEN 10 AND 30

と書くことができます。

範囲抽出 (◯以上□未満)

# Rails(ActiveRecord)
User.where(age: 10...30)
-- SQL
SELECT "users".* FROM "users" 
  WHERE "users"."age" >= 10 AND "users"."age" < 30