前提
社内でやろうとしてたことそのまま書けないので、
例えを探しましたが・・・わかりにくいかもですw
ポリモーフィックについては、こちらの記事がわかりやすかったです!感謝感激。
モデルは下記とします。
モデル図 *()の中がモデル名
普通自動車(Car)-<部品(Part)>-人(Owner)
Car
has_many :parts
has_many :owners, through: :parts
Part
belongs_to :cars
belongs_to :owners
Owner
has_many :parts
has_many :cars, through: :parts
目的は、
「ある人が所持している、ある普通自動車に搭載されている部品達をDBで管理できる」
とします。
もうちょっと具体的にいうと、
「AさんのIDから次郎号という自動車を特定できて、
かつ次郎号に搭載されている部品(前輪・後輪とか)も把握できる」
みたいな感じです。
異なるモデルも同一のモデルで管理したい
上記モデルをポリモーフィック可したらどうなるかというと
モデルはこんな感じになります。*()の中がモデル名
モデル図
なんかの乗り物-<部品(Part)>-人(Owner)
なんか乗り物(ここではCarとShipとしておきます)
has_many :parts
has_many :owners, as: :vehicle, through: :parts
Part
belongs_to :vehicles, polymorphic: true
belongs_to :owners
Owner
has_many :parts
has_many :cars, as: :vehicle, through: :parts
has_many :ships, as: :vehicle, through: :parts
要するに
「ある人が所持している、ある乗り物に搭載されている部品達をDBで管理できる」
何が良いの?となりますが
普通自動車以外の乗り物(バイク、船 etc...)の部品もPartモデルに突っ込めます!
Partモデルにvehicle_typeとvehicle_idというカラムを追加してあげることで
(vehicle_type = 'Car' とか vehicle_type = 'Ship'とか・・・)
Partからみたら、繋がりは一つに見えるのに色々な乗り物モデルと繋がれる
という状態になります。
何が大変だったか
さて、本題ですが
1つ前のセクションでポリモーフィック化は完了したとします。
となると、既存のソース内で変更が必要になってきます。
まず当時の私は、下記のクエリは問題ないと思っていました。(いつ使うんだこのクエリは!と言わないでくださいw)
「ある車に紐付いている、personを取り出す。条件は、男性で前輪を持っている人」
car = Car.find_by("123-456-789")
car.persons.joins(:parts).find_by('persons.sex = ? AND parts.name = ?', "male", "前輪")
これを叩くと、以下を含んだクエリが作成されます。
WHERE "parts"."car_id" = $1
ポリモーフィック化する以前は、Carモデルしかなかったのでこれで良かったのですが
ポリモーフィック後はvehicle_idというカラムに変わっているのでエラーになります。
ここで注意すべきは、「じゃあparts.vehicle_idにすればいいのでは?」・・それでは、事足りないということです。
具体的には、先ほどのクエリをこんな風にすればOKです。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
vehicle_idと合わせてvehicle_typeも引き合いに出すことで期待する値を確実に取ることができます。
まとめ
既存モデルをポリモーフィック化すると、そのモデルの先で叩くクエリを書き直さなければならない場合があります。
その際は、XXX_typeというようにクエリへモデルのタイプ(String)を含める必要があります。
おまけ: さらに抽象度を上げるには?
下記のクエリは、Carモデル用にハードコーディングされてます。
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', car.id, 'Car', "male", "前輪")
こんな風に変えるといい感じです!
Person.joins(:parts).find_by('vehicle_id = ? AND vehicle_type = ? AND persons.sex = ? AND parts.name = ?', vehicle.id, vehicle.class.to_s, "male", "前輪")
こうすれば、仮に変数vehicleにCarモデルが入ろうが、Shipモデルが入ろうが吸収してくれます。