今回の趣旨は、無駄に難しく考える必要はないということです。(勝手に悩み、ドツボにはまっておりました。)
例えば、下記のようなテーブルがあったとします。
例としてビールメーカーで作成しましたが、現実のものとは全く関係ありません。(妻夫木くんストラップも本当にあるかは知りません。)
companies(メーカー管理)
id | company_name |
---|---|
1 | キリン |
2 | アサヒ |
3 | サントリー |
4 | サッポロ |
products(各メーカーの商品管理)
id | company_id | product_name | price |
---|---|---|---|
1 | 1 | ラガービール | 100 |
2 | 1 | 一番搾り | 300 |
3 | 2 | スーパードライ | 200 |
4 | 3 | プレミアムモルツ | 300 |
5 | 3 | モルツ | 200 |
6 | 3 | プレミアムモルツブラック | 500 |
7 | 4 | サッポロビール | 200 |
campaigns(各メーカーの実施しているキャンペーン管理)
id | company_id | campaign_name | started_at | ended_at |
---|---|---|---|---|
1 | 1 | 缶ビール1本無料!! | 2013-12-01 00:00:00 | 2014-01-01 00:00:00 |
2 | 1 | 缶ビール1本無料!!(復活) | 2016-12-01 00:00:00 | 2017-01-01 00:00:00 |
3 | 4 | 妻夫木君ストラップ | 2016-12-10 00:00:00 | 2016-12-21 00:00:00 |
モデルは簡単に作成します。
class Company < ApplicationRecord
has_many :products
has_many :campaigns
end
class Product < ApplicationRecord
belongs_to :company
end
class Campaign < ApplicationRecord
belongs_to :company
end
ここで、メーカーの一覧を取得する際に、紐づく製品、キャンペーンも取得したいと思います。ただ、条件がありまして、productsからはpriceが500円以下(今回の例だと全てですが。。。)、campaignsからは今日開催しているものです。
製品はまだしも、キャンペーンはメーカーによっては開催していない場合もあります。
N+1問題を考慮しincludesで事前ロードしつつ、紐づくテーブルに条件を設定するため、下記の命令で実行しました。
Company.includes([:products,campaigns]).
where('products.price <= 500').
where('campaigns.started_at <= ?', Time.zone.now).
where('campaigns.ended_at <= ?', Time.zone.now).
references([:products, :campaigns])
発行されるSQLと結果はこちらになります。SELECT "companies"."id" AS t0_r0,
"companies"."company_name" AS t0_r1,
"companies"."created_at" AS t0_r2,
"companies"."updated_at" AS t0_r3,
"products"."id" AS t1_r0,
"products"."company_id" AS t1_r1,
"products"."product_name" AS t1_r2,
"products"."price" AS t1_r3,
"products"."created_at" AS t1_r4,
"products"."updated_at" AS t1_r5,
"campaigns"."id" AS t2_r0,
"campaigns"."company_id" AS t2_r1,
"campaigns"."campaign_name" AS t2_r2,
"campaigns"."started_at" AS t2_r3,
"campaigns"."ended_at" AS t2_r4,
"campaigns"."created_at" AS t2_r5,
"campaigns"."updated_at" AS t2_r6
FROM "companies"
LEFT OUTER JOIN "products" ON "products"."company_id" = "companies"."id"
LEFT OUTER JOIN "campaigns" ON "campaigns"."company_id" = "companies"."id"
WHERE (products.price <= 500)
AND (campaigns.started_at <= '2016-12-12 11:22:12.523389')
AND (campaigns.ended_at <= '2016-12-12 11:22:12.534654');
#<ActiveRecord::Relation [#<Company id: 1, company_name: "キリン", created_at: "2016-12-12 00:00:00", updated_at: "2016-12-12 00:00:00">]>
おかしい。。。キリンしか取得できない。。。発行したSQLを確認すると、JOINした後に条件指定しております。
ON句の中に入れられないかと思い、路頭に迷っているところで、下記のようにモデルを書き換えました。
class Company < ApplicationRecord
has_many :products
has_many :products_price_under_500, -> { where('products.price <= 500') }, class_name: 'Product'
has_many :campaigns
has_many :campaigns_published, -> { where('campaigns.started_at <= ?', Time.zone.now).where('campaigns.ended_at <= ?', Time.zone.now) }, class_name: 'Campaign'
end
# ProductとCampaignは変わらないので省略。
アソシエーションの際に条件指定して取得するという、非常に基本的なところを忘れておりました。一応、条件指定したくないケースもあると思うので、デフォルトとは別でつくりました。
これで書くと、先ほどのクエリも、短くなります。
Company.includes([:products_price_under_500,:campaigns_published])
SELECT "companies".* FROM "companies";
SELECT "products".* FROM "products" WHERE (products.price <= 500) AND "products"."company_id" IN (1, 2, 3, 4);
SELECT "campaigns".* FROM "campaigns" WHERE (campaigns.started_at <= '2016-12-12 11:30:03.468617')
AND (campaigns.ended_at <= '2016-12-12 11:30:03.469799') AND "campaigns"."company_id" IN (1, 2, 3, 4);
#<ActiveRecord::Relation [#<Company id: 1, company_name: "キリン", created_at: "2016-12-12 00:00:00", updated_at: "2016-12-12 00:00:00">,
#<Company id: 2, company_name: "アサヒ", created_at:-12-12 00:00:00", updated_at: "2016-12-12 00:00:00">,
#<Company id: 3, company_name: "サントリー", created_at: "2016-12-12 00:00:00", updated_at: "2016-12-12 00:00:00">,
#<Company id: 4, compane: "サッポロ", created_at: "2016-12-12 00:00:00", updated_at: "2016-12-12 00:00:00">]>
発行されるSQLは3本ですが、N+1問題を回避しつつ、条件指定できているかと思います。もし、同じようなことをやられている方がいらっしゃいましたら、ぜひ参考にしてください。(反面教師的な意味でも。)
0 件のコメント:
コメントを投稿