ActiveRecordのorを間違って使ってしまった話

3 min read読了の目安(約3000字

こんにちは、いっせいです。

みなさんActiveRecordは使いこなしていますか?
私も日々Railsでアプリケーションを開発し、ActiveRecordを使ってはいるのですが気を抜くとすぐにドハマリします。。。

今回は先日使い方を誤り、不具合を出してしまったAcitiveRecordのorについて反省の意を込めて書こうと思います。

or

http://api.rubyonrails.org/v5.2.5/classes/ActiveRecord/QueryMethods.html#method-i-or

サンプルには以下のようにかいてあります

Post.where("id = 1").or(Post.where("author_id = 3"))
# SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))

and とは違い、条件ではなく対象のモデルからリレーションを作ってあげる必要があります。

今回の事象

リレーション

実際のコードとは違いますが、だいたい以下のような構造のモデルとリレーションがありました。

  • Classroom
    • 教室
  • Questionnaire
    • とあるアンケート
    • 「first_request_date」「second_request_date」「third_request_date」と第一から第三希望の日時を記入
  • Student
    • 生徒

ClassroomとQuestionnaireは1対多の関係にあります。
StudentとQuestionnaireは1対多の関係にあります。

class Classroom  < ApplicationRecord
  has_many :questionnaires
end

class Questionnaire < ApplicationRecord
  belongs_to :classroom
  belongs_to :student
end

class Student  < ApplicationRecord
  has_many :questionnaires
end

やりたかったこと

とある教室のアンケートで希望日のいずれかが検索したい日付のものを取得するメソッドを書きたかったのです。

生成されるクエリは以下のようなものを想定していました。

SELECT * FROM questionnaires
WHERE 
  classroom_id = xx
  AND
  (
    first_requst_date = 'xxxx-xx-xx'
    OR
    second_request_date = 'xxxx-xx-xx'
    OR
    third_request_date = 'xxxx-xx-xx'
  );

そこでClassroomに以下のようなメソッドを実装しました。

  def search_questionnaires_by_request_date(date)
   questionnaires
     .where(first_request_date: date)
     .or(Questionnaire.where(second_request_date: date))
     .or(Questionnaire.where(third_request_date: date))
  end

ORの使い方的に「絞り込むクラス名合わせたし大丈夫!」と思っていました。
お気づきの方はお気づきかと思いますが、これは意図通りのSQLが作成されません。

> classroom = Classroom.first
> classroom.search_questionnaires_by_request_date("2021-06-01").to_sql
=>
"SELECT 
  \"questionnaires\".* 
FROM
  \"questionnaires\"
WHERE
(
  (
    \"questionnaires\".\"classroom_id\" = 1 
   AND 
   \"questionnaires\".\"first_request_date\" = '2021-06-01'
   OR 
   \"questionnaires\".\"second_request_date\" = '2021-06-01'
  ) 
  OR
  \"questionnaires\".\"third_request_date\" = '2021-06-01'
)"

この実装では他の教室の生徒のアンケートが取得されてしまいます。。。

意図通りにするにはorにもアソシエーションを渡してあげる必要があります。

scope :search_by_request_date, ->(date) { 
  questionnaires
    .where(first_request_date: date)
    .or(questionnaires.where(second_request_date: date)
    .or(questionnaries.where(third_request_date: date) 
 }
> classroom = Classroom.first
> classroom.search_questionnaires_by_request_date("2021-06-01").to_sql
=> 
"SELECT
  \"questionnaires\".* 
FROM
  \"questionnaires\" 
WHERE 
  \"questionnaires\".\"classroom_id\" = 1
  AND 
  (
    (
      \"questionnaires\".\"first_request_date\" = '2021-06-01' 
      OR
      \"questionnaires\".\"second_request_date\" = '2021-06-01'
    ) 
    OR 
    \"questionnaires\".\"third_request_date\" = '2021-06-01'
  )"

とする必要があります。

みなさんもORの挙動をしっかり把握して意図通りのクエリが発行されるようにしましょう!
私の二の舞にならないでください。。。 :sob:

ORの使い方はもっとこういうのがいいよ!というのがあればぜひ https://twitter.com/ise_tang までご連絡ください!

それではまた!