前提
RailsでWebアプリ開発をしており、関連のあるテーブルの外部結合を試みています。
外部結合の結合条件として、eager_loadでデフォルトの [テーブル1].id = [テーブル2].テーブル1_id のほか、
自身で結合条件を追加したいのですが、うまくいきません。
開発環境
- OS: Linux (Windows Subsystem for Linux)
- 言語: ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
- フレームワーク: Rails 6.0.3.4
- DBMS: mariadb Ver 15.1 Distrib 10.1.48-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
実現したいこと
eager_loadメソッドを使った外部結合をするにあたり、結合条件に動的な値を使いたいです。
詳細
例として、以下のような3つのテーブルを想定します。
- 利用者テーブル(users)
id | name | created_at | updated_at |
---|---|---|---|
1 | 利用者1 | 2022/10/25 11:46 | 2022/10/25 11:46 |
2 | 利用者2 | 2022/10/25 11:47 | 2022/10/25 11:47 |
3 | 利用者3 | 2022/10/25 11:47 | 2022/10/25 11:47 |
- 貸出履歴テーブル(lending_records)
id | user_id | book_id | lent_at | returned_at | created_at | updated_at |
---|---|---|---|---|---|---|
1 | 1 | 1 | 2022-10-01 9:00 | 2022-10-15 | 2022/10/25 11:47 | 2022/10/25 11:47 |
2 | 1 | 2 | 2022-10-01 9:00 | 2022-10-15 | 2022/10/25 11:47 | 2022/10/25 11:47 |
3 | 2 | 1 | 2022-10-16 15:30 | 2022-10-30 | 2022/10/25 11:48 | 2022/10/25 11:48 |
- 蔵書テーブル(books)
id | title | author | arrived_at | created_at | updated_at |
---|---|---|---|---|---|
1 | 吾輩は猫である | 夏目漱石 | 2022-09-30 | 2022/10/25 11:47 | 2022/10/25 11:47 |
2 | たけくらべ | 樋口一葉 | 2022-09-30 | 2022/10/25 11:47 | 2022/10/25 11:47 |
3 | 人間失格 | 太宰治 | 2022-10-20 | 2022/10/25 11:47 | 2022/10/25 11:47 |
このとき、eager_loadメソッドを使って、以下のSQLにおいて
- 結合条件
`lending_records`.`book_id` = 2 AND
の追加 - 動的なbook_idの指定
をしたいです。
SQL
1SELECT 2 `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, 3 `lending_records`.`id` AS t1_r0, `lending_records`.`user_id` AS t1_r1, `lending_records`.`book_id` AS t1_r2, `lending_records`.`lent_at` AS t1_r3, 4 `lending_records`.`returned_at` AS t1_r4, `lending_records`.`created_at` AS t1_r5, `lending_records`.`updated_at` AS t1_r6, 5 `books`.`id` AS t2_r0, `books`.`title` AS t2_r1, `books`.`author` AS t2_r2, `books`.`arrived_at` AS t2_r3, `books`.`created_at` AS t2_r4, `books`.`updated_at` AS t2_r5 6FROM `users` 7LEFT OUTER JOIN `lending_records` ON `lending_records`.`book_id` = 2 AND `lending_records`.`user_id` = `users`.`id` 8LEFT OUTER JOIN `books` ON `books`.`id` = `lending_records`.`book_id`
- SQLの実行結果
t0_r0 | t0_r1 | t1_r0 | t1_r1 | t1_r2 | t1_r3 | t1_r4 | t2_r0 | t2_r1 | t2_r2 | t2_r3 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 利用者1 | 2 | 1 | 2 | 2022/10/1 9:00 | 2022/10/15 | 2 | たけくらべ | 樋口一葉 | 2022-09-30 |
2 | 利用者2 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
3 | 利用者3 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
(画面幅を考慮し、各テーブルのcreated_at, updated_at列は省略しています)
似た状況のように思われる質問
https://stackoverflow.com/questions/41532444/eager-load-for-custom-joins-in-activerecord
試したこと
user.rb の has_many :lending_records
行へのスコープ追加。
- 各モデルファイル
Ruby
1# user.rb 2class User < ApplicationRecord 3 has_many :lending_records, -> { self.where(book_id: 2) } 4end
Ruby
1# lending_record.rb 2class LendingRecord < ApplicationRecord 3 belongs_to :user 4 belongs_to :book 5end
Ruby
1# book.rb 2class Book < ApplicationRecord 3 has_many :lending_records 4end
- 実行したコード
Ruby
1User.eager_load(lending_records: :book)
この方法で、実現したいことに記載したSQLが発行され、期待通りの結果が得られることは分かりました。
しかし、この方法ではbook_idを動的に指定することができません。
has_manyのスコープの中で変数を扱うことはできるのでしょうか。
補足情報
以下①は想定通りにレコードが取れず、②は使うことを避けたいです。
① 結合した後にWHERE句で絞り込む
② joinsメソッドで結合条件を付加する
① 結合した後にWHERE句で絞り込む
この場合は以下の結果になります。
- モデルファイル
Ruby
1# user.rb 2class User < ApplicationRecord 3 has_many :lending_records 4end
(lending_record.rb および book.rb は上記と同様)
- 実行したコード
Ruby
1User.eager_load(lending_records: :book).where(lending_records: { book_id: 2 })
- 発行されるSQL
SQL
1SELECT 2 `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, 3 `lending_records`.`id` AS t1_r0, `lending_records`.`user_id` AS t1_r1, `lending_records`.`book_id` AS t1_r2, `lending_records`.`lent_at` AS t1_r3, 4 `lending_records`.`returned_at` AS t1_r4, `lending_records`.`created_at` AS t1_r5, `lending_records`.`updated_at` AS t1_r6, `books`.`id` AS t2_r0, 5 `books`.`title` AS t2_r1, `books`.`author` AS t2_r2, `books`.`arrived_at` AS t2_r3, `books`.`created_at` AS t2_r4, `books`.`updated_at` AS t2_r5 6FROM `users` 7LEFT OUTER JOIN `lending_records` ON `lending_records`.`user_id` = `users`.`id` 8LEFT OUTER JOIN `books` ON `books`.`id` = `lending_records`.`book_id` 9WHERE `lending_records`.`book_id` = 2
- SQLの実行結果
t0_r0 | t0_r1 | t1_r0 | t1_r1 | t1_r2 | t1_r3 | t1_r4 | t2_r0 | t2_r1 | t2_r2 | t2_r3 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 利用者1 | 2 | 1 | 2 | 2022/10/1 9:00 | 2022/10/15 | 2 | たけくらべ | 樋口一葉 | 2022-09-30 |
(画面幅を考慮し、各テーブルのcreated_at, updated_at列は省略しています)
このように、結合条件に指定した場合とは異なる結果となります。
② joinsメソッドで結合条件を付加する
作成しているWebアプリでは、例えば
Ruby
1model = User.eager_load(lending_records: :book).joins("AND `lending_records`.`book_id` = 2") 2if hoge.nil? 3 model = model.eager_load(:other_tables) 4end
といった形でチェーンして使うことを想定しています。
この場合に、LEFT OUTER JOIN句はチェーンした順でSQLに追加されていく仕様かと思います。
そのためjoinsメソッドで書いた文字列が期待する位置にならないおそれがあることからjoinsメソッドでの対応は避けたいです。
(.eager_load(lending_records: :book)を必ず最後にチェーンするようにすれば期待通りのSQLが得られるかとは思いますが、保守の観点から避けたい)
以上、よろしくお願いいたします。