RailsのActiveRecordでポリモーフィック関連を別のモデルからhas_manyする
まえおき
- Article
- ArticleSnapshot
- article_id
- Like
- target_type
- target_id
ArticleとLikeであれば話は単純だ。
class Article < ActiveRecord::Base
has_many :likes, as: :target
end
class Like < ActiveRecord::Base
belongs_to :target, polymorphic: true
end
ただ、諸般の事情によりArticleはレコード削除してしまったが、そのスナップショットであるArticleSnapshotからどうしてもLikeをたどりたい。
article_snapshot: ArticleSnapshot(id: 1111, article_id: 123) があったときに、article_snapshot.likes すると
SELECT * FROM likes
WHERE target_type = 'Article' AND target_id = 123
こういうクエリが叩かれるようにしたい。
Railsのソースななめよみ
まずは、当然うまくいかないのだが、以下のようなアソシエーションを定義して動作を見てみる。
class ArticleSnapshot < ActiceRecord::Base
has_many :likes, as: :target
end
has_one, has_manyなどは、builderでメソッドが定義される。
article_snapshot.association(:likes)
のようにRailsコンソールで叩くと、Associationクラスの子クラスが見える。
ownerにarticle_snapshot, reflectionにHasManyReflection のようなクラスがある。
article_snapshot.association(:likes).reader
と叩くと、article_snapshot.likesを叩いたときと同じ結果が返ってくるので、Associationクラスの #reader
メソッドを叩いている際にクエリが作られるっぽい。
どうも find_target
というメソッドがクエリを作る際のスコープを指定してる場所のようだ。
binds = AssociationScope.get_bind_values(owner, reflection.chain)
ここで、ポリモーフィック関連の [1111, 'ArticleSnapshot']
が作られており、それがそのまま
SELECT * FROM likes
WHERE target_type = 'ArticleSnapshot' AND target_id = 1111
というクエリを発行するもととなっている。
1111 は、Reflectionクラスの #join_id_for
これは primary_keyオプションが指定されていたらそれが使われるようだ。試しに
class ArticleSnapshot < ActiceRecord::Base
has_many :likes, as: :target, primary_key: :article_id
end
こうしてみると、get_bind_valuesの返り値は [123, 'ArticleSnapshot']
になる。
問題はtarget_typeの方だが、これは owner.class.polymorphic_name
を参照している。つまり、ArticleSnapshot.polymorphic_name がそのまま使われている。
小規模で影響範囲が小さいプロダクトであれば、polymorphic_nameをオーバーライドしてしまうということも考えうるが、has_many1個のためだけにクラス名定義を変えてしまうのはちょっとやりすぎだろう。
結論。
ポリモーフィック関連をたどるasは使えない。愚直にやるしかない。
class ArticleSnapshot < ActiceRecord::Base
has_many :likes,
-> { where(target_type: 'Article') },
foreign_key: :target_id,
primary_key: :article_id
end
ここまでやれば、ベースのスコープで WHERE target_type = 'Article' AND target_id = ?
相当のものが作られ、get_bind_valuesではarticle_idの123が使われる。
Discussion