Joins
The mechanism
Poly::Joins is an ActiveSupport::Concern. Including it into a model runs
its included do ... end block:
module Poly::Joins extend ActiveSupport::Concern
included do define_polymorphic_joins! end
# ...enddefine_polymorphic_joins! walks the includer’s belongs_to associations
and, for each one declared polymorphic: true, defines a class method named
:"joins_#{assoc_name}" — for example, a model with
belongs_to :commentable, polymorphic: true gets joins_commentable. If a
method of that name is already defined (e.g. from a previous include), it’s
skipped rather than redefined, so re-including Poly::Joins on the same
model is a no-op.
The generated method
The generated joins_<assoc>(klass) method takes the target AR class
(not an instance) — for example, Comment.joins_commentable(Post), not
Comment.joins_commentable(some_post). It resolves klass.base_class, then
returns an ActiveRecord::Relation with an INNER JOIN added on:
source[:"#{assoc}_id"].eq(target[:id]).and(source[:"#{assoc}_type"].eq(base_klass.name))Because it returns an ActiveRecord::Relation, the result is chainable with
further scopes like .where — see the worked example below.
The required reverse association
The class passed to joins_<assoc> must declare a reverse has_many or
has_one ..., as: :<assoc> association back to the includer. This is
checked via the private join_allowed? class method before the join is
built. If the reverse association isn’t declared, joins_<assoc> raises
Poly::PolymorphicJoinError instead of silently building a join against a
class that doesn’t actually participate in the polymorphic relationship.
Poly::PolymorphicJoinError
Poly::PolymorphicJoinError is a plain StandardError subclass with no
methods of its own:
class Poly::PolymorphicJoinError < StandardError; endIt’s raised from two places inside a generated joins_<assoc> method:
-
The argument isn’t an ActiveRecord model (
klass <= ActiveRecord::Baseis false):Expected an ActiveRecord model -
The argument’s class doesn’t declare the required reverse association:
Polymorphic join requires #{base_klass} to declare: has_many :#{name.underscore.pluralize}, as: :#{assoc_name}For example, calling
Comment.joins_commentable(SomeClass)whereSomeClasshas nohas_many :comments, as: :commentableraises:Polymorphic join requires SomeClass to declare: has_many :comments, as: :commentable
To fix it: add the stated has_many (or has_one) ..., as: :<assoc>
association on the target class.
A deprecated top-level PolymorphicJoinError constant still works as an
alias to Poly::PolymorphicJoinError, so existing rescue PolymorphicJoinError
/ is_a?(PolymorphicJoinError) callers keep working across the 1.2.0 rename
— but new code and docs should reference Poly::PolymorphicJoinError
directly.
A worked example: Comment, Post, and User
This example uses a single polymorphic association, Comment#commentable,
that can point at either a Post or a User:
class Post < ApplicationRecord has_many :comments, as: :commentableend
class User < ApplicationRecord has_many :comments, as: :commentableend
class Comment < ApplicationRecord belongs_to :commentable, polymorphic: true
include Poly::JoinsendIncluding Poly::Joins on Comment generates Comment.joins_commentable,
usable against either reverse-associated class:
Comment.joins_commentable(Post) # => an ActiveRecord::RelationComment.joins_commentable(User) # => an ActiveRecord::RelationThe generated SQL for the Post join includes the join itself and a
commentable_type predicate:
Comment.joins_commentable(Post).to_sql# includes: INNER JOIN "posts"# includes: "comments"."commentable_id" = "posts"."id"# includes: "comments"."commentable_type" = 'Post'Being an ActiveRecord::Relation, it’s chainable with further scopes:
Comment.joins_commentable(Post).where(posts: { title: "Hello" })The error case
If the class passed to joins_commentable doesn’t declare the reverse
association, Poly::PolymorphicJoinError is raised with the message pattern
described above. For example, a class without has_many :comments, as: :commentable:
class Unrelated < ApplicationRecord self.table_name = "posts"end
Comment.joins_commentable(Unrelated)# raises Poly::PolymorphicJoinError:# "Polymorphic join requires Unrelated to declare: has_many :comments, as: :commentable"Next Steps
Continue with Role to give a polymorphic association semantic
meaning via a normalized role column.