Edited at

has_many :throughの結合モデルの属性を関連先モデルの属性として取得する

More than 1 year has passed since last update.


やること

あるユーザが画像群の中から複数枚を選んでプロフィール画像として設定できるような、次のテーブルがあるとします。特徴としては、結合モデル profile_images に外部キー以外の属性 order があることが挙げられます。

これをActive Recordで書くと次のようになります。

class User < ApplicationRecord

has_many :profile_images
has_many :images, through: :profile_images
end

class ProfileImage < ApplicationRecord
belongs_to :users
belongs_to :images
end

# Image は省略

このとき、結合モデル ProfileImage の属性 order を取得する場合と、関連先のモデル Image の属性 src を取得する場合とで、次のように書き分けなければなりません。

user.profile_images.first.order

user.images.first.src

記述を統一させるために、User のレコードから関連先 Image をたどる場合は、次のように ProfileImage の属性も Image の属性も統一した形で取得できるようにします。

user.ordered_images.first.order # User からたどると Image から ProfileImage#order を参照できる

user.ordered_images.first.src


やりかた

has_many の第2引数 (scope) に次のようなラムダ式を渡します。この引数は関連先を取得するときに使うクエリををカスタマイズするために使います。

class User < ApplicationController

has_many :ordered_images,
->{ select("#{Image.table_name}.*", "#{ProfileImage.table_name}.order AS order") },
through: :profile_images,
class_name: 'Image'
end

※注意: ここでは普通の関連と区別するために ordered_images という名前にしています。また、この方法を使うと、user.ordered_images.count のような集約操作はクエリをうまく生成できずエラーになります。

ここでは、テーブル users, profile_images, images をJOINしたテーブルをもとに関連先 Image のオブジェクト群を取得するときに、ラムダ式内の selectImage の属性に加えて ProfileImage.order を合わせてSELECTするようにしています。

これで user.ordered_images.first.order のように、User レコードから関連をたどったときは関連先モデルが結合モデルの属性を持つかのように取り扱うことができます。


参考資料