👌
中間モデルの属性を関連先インスタンスの属性として取り出す(ActiveRecord)
たとえば、次のようなケース
- 買い物かご
Backet
- 商品
Item
- 買い物かごに入っている状態を表す中間モデル
- 入っている数
quantity
を中間モデルが持つ
このとき、買い物かごの商品リストと数量を取り出すには、どう書けばいい?と質問がありました。
class Backet < ApplicationRecord
has_many :backet_items, dependent: :destroy
has_many :items, through: :backet_item
end
class Item < ApplicationRecord
validates :name, presence: true
validates :price, presence: true
end
class BacketItem < ApplicationRecord
belongs_to :backet
belongs_to :item
validates :quantity, presence: true
end
# こうではなく
backet.backet_items.includes(:item).each do |backet_item|
p backet_item.item.name # => りんご
p backet_item.item.price # => 250
p backet_item.quantity # => 5
end
# こうしたい (itemsではなくbacket_itemsのカラムの値をとりたい)
backet.items.each do |item|
p item.name # => りんご
p item.price # => 250
p item.quantity # => 5
end
select
has many through: の読み込みでは、自動で中間テーブルがJOINされます。
puts backet.items.to_sql
# SELECT "items".* FROM "items" INNER JOIN "backet_items" ON "items"."id" = "backet_items"."item_id" WHERE "backet_items"."backet_id" = 'c2d10446-33a0-48ad-8f1b-e4c5f489fffa'
=>
このため、中間テーブルのカラムはクエリメソッドの select
で選択できます。
backet.items.select(:quantity, Item.arel_table[Arel.star]).each do |item|
p item.name # => りんご
p item.price # => 250
p item.quantity # => 5 取り出せた
end
# Item Load (1.3ms) SELECT "quantity", "items".* FROM "items" INNER JOIN "backet_items" ON "items"."id" = "backet_items"."item_id" WHERE "backet_items"."backet_id" = $1 [["backet_id", "c2d10446-33a0-48ad-8f1b-e4c5f489fffa"]]
select
はブロックを渡すと Array#select
と同様に振る舞い、引数を渡すとSELECT句を指定できます。
ただし、取得のたびに select(:quantity, Item.arel_table[Arel.star])
を書くのは冗長でDRYではありませし、読みづらいですね。
A. default_scope 引数を使う
has_many
メソッドの default_scope で select
を使うことでDRYになります。
class Backet < ApplicationRecord
has_many :backet_items, dependent: :destroy
has_many :items, -> { select(:quantity, arel_table[Arel.star]) }, through: :backet_items
end
backet.items.each do |item|
p item.name # => りんご
p item.price # => 250
p item.quantity # => 5
end
ただ、default scope は慎重に扱う必要があります。
暗黙的に Item
に quantity
というメソッドが生えるのも気持ち悪いと感じるかもしれません。
B. has_many extension引数を使う
has_many
メソッドの extension を使うと、クエリメソッドを追加できます。
これを使うと、人間が読みやすい形で、必要な時にだけ select
を追加できます。
class Backet < ApplicationRecord
has_many :backet_items, dependent: :destroy
has_many :items, through: :backet_items do
def with_quantity
select(:quantity, arel_table[Arel.star])
end
end
end
# with_quantity で quantity を取得することを明示できる
backet.items.with_quantity.each do |item|
p item.name # => りんご
p item.price # => 250
p item.quantity # => 5 取り出せた
end
# => Item Load (0.5ms) SELECT "quantity", "items".* FROM "items" INNER JOIN "backet_items" ON "items"."id" = "backet_items"."item_id" WHERE "backet_items"."backet_id" = $1 [["backet_id", "c2d10446-33a0-48ad-8f1b-e4c5f489fffa"]]
サンプルコード
まとめ
select
クエリメソッドを使って選択することで、モデルに対応したテーブル以外のカラムも、モデルの属性と同じように扱うことができました。
また、デフォルトスコープやエクステンションを使うことで、読みやすく書きやすくすることができました。
Discussion