ActiveRecordのorを間違って使ってしまった話
こんにちは、いっせいです。
みなさんActiveRecordは使いこなしていますか?
私も日々Railsでアプリケーションを開発し、ActiveRecordを使ってはいるのですが気を抜くとすぐにドハマリします。。。
今回は先日使い方を誤り、不具合を出してしまったAcitiveRecordのorについて反省の意を込めて書こうと思います。
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 までご連絡ください!
それではまた!
Discussion