Bad combo in ActiveRecord: polymorphic relationships, auto-generated numeric IDs and `includes`

Minh Reigen
2 min readNov 23, 2023
Photo by Markus Spiske on Unsplash

Navigating the complexities of polymorphic relationships, auto-generated numeric IDs, and the use of “includes” in queries can indeed turn into a wild party in my model. The particular challenge I encountered involves a model called Selectionwith two polymorphic associations: bundle and product

Here’s the snippet that caused the conundrum:

class Selection < ApplicationRecord
belongs_to :bundle,
-> { includes(:selections).where(selections: { selectionable_type: "Bundle" }) },
foreign_key: "selectionable_id",
inverse_of: :selections,
optional: true
belongs_to :product,
-> { includes(:selections).where(selections: { selectionable_type: "Product" }) },
foreign_key: "selectionable_id",
inverse_of: :selections,
optional: true
end

The issue surfaces when I attempt a query like Selection.joins(:bundle) and encounter an unexpected LEFT OUTER JOIN

SELECT
`selections`.*
FROM
`selections`
INNER JOIN
`bundles` ON `bundles`.`id` = `selections`.`selectionable_id`
LEFT OUTER JOIN
`selections` `selections_bundles` ON
`selections_bundles`.`bundle_id` = `bundles`.`id` AND
`selections`.`selectionable_type` = 'Bundle'

The culprit lies in the belongs_to declarations where I’ve used includes(:bundle)

To resolve this and ensure an INNER JOIN in my queries, I replace the includes(:bundle) with joins(:bundle):

class Selection < ApplicationRecord
belongs_to :bundle,
-> { joins(:selections).where(selections: { selectionable_type: "Bundle" }) },
foreign_key: "selectionable_id",
inverse_of: :selections,
optional: true

belongs_to :product,
-> { joins(:selections).where(selections: { selectionable_type: "Product" }) },
foreign_key: "selectionable_id",
inverse_of: :selections,
optional: true
end

With this modification, my queries, like Selection.joins(:bundle), will now generate the desired INNER JOIN:


SELECT
`selections`.*
FROM
`selections`
INNER JOIN
`bundles` ON `bundles`.`id` = `selections`.`selectionable_id`
INNER JOIN
`selections` `selections_bundles` ON
`selections_bundles`.`bundle_id` = `bundles`.`id`
AND `selections`.`selectionable_type` = 'Bundle'

Now, my model’s party with polymorphic relationships, auto-generated IDs, and queries should be a bit more well-behaved. Cheers to smoother database interactions!

--

--