今回はActiveRecordのreferencesメソッドについて挙動を確認したので、それをまとめたいと思います。
referencesメソッドとは
API docには以下のように定義されていました。
指定されたtable_namesがSQL文字列によって参照されているため、個別にロードするのではなく、任意のクエリでJOINする必要があることを示すために使用します。
https://apidock.com/rails/ActiveRecord/QueryMethods/references
どういうことなのかを実際にメソッドを使いながら見ていきます。
記事一覧からコメントの内容で検索する例を見てみます。
Article.includes(:comments).where("comments.content = 'a'")
=> Article Load (3.8ms) SELECT `articles`.* FROM `articles` WHERE (comments.created_at > 31535999.99995604)
Article Load (3.4ms) SELECT `articles`.* FROM `articles` WHERE (comments.created_at > 31535999.99995604) /* loading for inspect */ LIMIT 11
#<Article::ActiveRecord_Relation:0x4164>
エラーになって取得できないことが分かるかと思います。これは検索の際にテーブル名がSQL文字列によって参照されているためです。アソシエーションによって参照するようにすると、取得できます。
Article.includes(:comments).where(comments: { content: 'a' })
=> CACHE SQL (0.2ms) SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`content` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `articles`.`is_public` AS t0_r5, `comments`.`id` AS t1_r0, `comments`.`content` AS t1_r1, `comments`.`article_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4, `comments`.`user_id` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `comments`.`content` = 'a'
[#<Article:0x0000ffff90c82108
id: 1,
title: "記事1",
content: "a",
created_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
updated_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
is_public: true>]
少し反れましたが、referencesメソッドを使うことで正常に検索することができます。
Article.includes(:comments).references(:comments).where("comments.content = 'a'")
=> SQL (8.1ms) SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`content` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `articles`.`is_public` AS t0_r5, `comments`.`id` AS t1_r0, `comments`.`content` AS t1_r1, `comments`.`article_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4, `comments`.`user_id` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE (comments.content = 'a')
[#<Article:0x0000ffff9096cf18
id: 1,
title: "記事1",
content: "a",
created_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
updated_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
is_public: true>]
referencesは比較条件を使うときなど、SQL文字列を使って検索したいケースなどで有用です。
Article.includes(:comments).references(:comments).where('comments.created_at > ?', Time.current - 1.year.ago)
=> SQL (25.0ms) SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`content` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `articles`.`is_public` AS t0_r5, `comments`.`id` AS t1_r0, `comments`.`content` AS t1_r1, `comments`.`article_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4, `comments`.`user_id` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE (comments.created_at > 31535999.99946196)
[#<Article:0x0000ffff90ce9f38
id: 1,
title: "記事1",
content: "a",
created_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
updated_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
is_public: true>]
この挙動については、Railsのソースコード中でも例を交えて解説してくれています。この辺りが丁寧でRailsはいいですね。
mergeメソッドとの併用
もう一つ僕が実務で見てきたreferencesメソッドの使いどころはmergeメソッドと一緒に使うケースです。
mergeメソッドは2つのクエリをマージして一つのSQLにするメソッドです。mergeメソッドの使用例を見てみます。
Article.includes(:comments).where(comments: {content: 'a'}).merge(Comment.where(user_id: 1))
=> CACHE SQL (0.1ms) SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`content` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `articles`.`is_public` AS t0_r5, `comments`.`id` AS t1_r0, `comments`.`content` AS t1_r1, `comments`.`article_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4, `comments`.`user_id` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id`
WHERE `comments`.`content` = 'a' AND `comments`.`user_id` = 1
[#<Article:0x0000aaaaf93142b0
id: 1,
title: "記事1",
content: "a",
created_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
updated_at: Sat, 19 Feb 2022 16:23:48.060743000 JST +09:00,
is_public: true>]
SQL文のWHERE句がWHERE comments.content = 'a' AND comments.user_id = 1
となっていて、mergeメソッドによって条件が結合されているのが分かるかと思います。
このとき、where句で絞り込みがされておらずincludesしているだけの場合、テーブルが結合されていないので、mergeすることができません。
Article.includes(:comments).merge(Comment.where(user_id: 1))
=> Article Load (1.0ms) SELECT `articles`.* FROM `articles` WHERE `comments`.`user_id` = 1
Article Load (1.2ms) SELECT `articles`.* FROM `articles` WHERE `comments`.`user_id` = 1 /* loading for inspect */ LIMIT 11
#<Article::ActiveRecord_Relation:0x3520>
この場合、referencesメソッドを使うことで、mergeすることができます。
Article.includes(:comments).references(:comments).merge(Comment.where(user_id: 1))
=> SQL (11.0ms) SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`content` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `articles`.`is_public` AS t0_r5, `comments`.`id` AS t1_r0, `comments`.`content` AS t1_r1, `comments`.`article_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4, `comments`.`user_id` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `comments`.`user_id` = 1
[]
※ データベースをリセットした関係で結果が返ってきてませんが、正常に取得するSQLが走っているのが分かるかと思います。
このように、mergeメソッドと組み合わせて使うのが、referencesメソッドのもう一つのユースケースです。
まとめ
referencesメソッドを使うユースケースとしては以下の2点です。
- includesしたテーブルのカラムをSQL文字列を使って検索をする場合に利用する
- mergeメソッドでincludes先のテーブルを使い、かつWHERE句などを用いてテーブルを結合していない場合に利用する
まだ他のユースケース等あればコメント等で教えていただけるとうれしいです。
Railsにはまだまだ知らない便利なメソッドがたくさんあると思うので、もっと勉強していきたいと思いました。
ここまで読んでいただきありがとうございました。