Join to a a single resource type in a polymorphic assocaition

tl; dr

belongs_to :resource, polymorphic: true is great, but don’t tell me you’ve never wanted to scope that to where(resource_type: "Foo"). To use Bar.joins(:foo) when :foo is one of many resource_types.

You probably already found out you can’t bar.joins(:resource). This make sense, SQL can’t perform a join on each row to multiople table depending on resource_type. The resulting joined table structure would be nonsensical. But in this case, you only want to join to the foos table, so why can’t you do it? Because .joins(:resource) needs to evaluate to something, and it can’t.

Write your own join? Then you lose all the benefits of Rails associations, including eager loading.

Somewhere ((here)[]?) you may have found a solution I did as well:

class Bar
  belongs_to :foo, -> { where(bars: {resource_type: 'Foo'}) }, foreign_key: 'resource_id'
end

Yay:

Bar.joins(:foo)
# => #<ActiveRecord::Relation [#<Bar id: 1, resource_type: "Foo", resource_id: 1, foo_id: 1, deleted: false>]>

Boo:

Bar.first.foo
# => ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'bars.resource_type' in 'where clause': SELECT  `foos`.* FROM `foos` WHERE `foos`.`deleted` = 0 AND `foos`.`id` = 1 AND `bars`.`resource_type` = 'Foo' LIMIT 1

Huh? No column bars.resource_type? Sure: the SQL generated by the association getter method only looks for a Foo with the matching id. It doesn’t join to, and therefore doesn’t have access to, the bars table. However we have added a scope that references the bars table that we are not joining to, and so the SQL statement balks. So that means the fix is simple:

class Bar
  belongs_to :foo, -> { joins(:bar).where(bars: {resource_type: 'Foo'}) }, foreign_key: 'resource_id'
end

Provided, of course, the :bar assocaiton is set up correclty on class Foo. Note that this may be a has_one or a has_many for your application, and for the latter of course you need to joins(:bars) instead of joins(:bar).

Of course we could just use Bar.first.resource, but it doesn’t feel right to leave this dangling error-prone method out there.

Yay:

Bar.first.foo
# => #<Foo id: 1, deleted: false>
Bar.preload(:foo).first
# => #<Bar id: 1, resource_type: "Foo", resource_id: 1, foo_id: 1, deleted: false>

So we’re done … ? Nope. There’s one more complication. Let’s add another resource_type to the mix:

class Bar
  belongs_to :foo, -> { joins(:bar).where(bars: {resource_type: 'Foo'}) }, foreign_key: 'resource_id'
  belongs_to :baz, -> { joins(:bar).where(bars: {resource_type: 'Baz'}) }, foreign_key: 'resource_id'
end

baz = Baz.create
bar2 = Bar.new
bar2.resource = baz
bar2.save

The existence of bar2 is critical in disvoering the following bug, which we’ll explain just below.

Boo:

Bar.first
# => #<Bar id: 1, resource_type: "Foo", resource_id: 1, foo_id: 1, deleted: false>
Bar.first.resource
# => #<Foo id: 1, deleted: false>
Bar.first.foo
# => #<Foo id: 1, deleted: false>
Bar.first.baz
# => #<Baz id: 1>

Bar.first’s resource is a Foo, not a Baz, and yet it’s incorrectly returning to us a Baz when we ask for it! Bar.first does not have a Baz, so we should be getting nil here. What’s happening is that when ActiveRecord is generates the query for bar.baz, once again it drives the query off of the bazes table with poor regard for the bars table:

SELECT  `bazs`.* FROM `bazs` INNER JOIN `bars` ON `bars`.`resource_id` = `bazs`.`id` AND `bars`.`deleted` = 0 AND `bars`.`resource_type` = 'Baz' WHERE `bazs`.`id` = 1 AND `bars`.`resource_type` = 'Baz' LIMIT 1

MySQL is joining bazs to bars, because we told it to, fine, and it’s checking for resource_type = 'Baz'. So what’s going wrong? Let’s take a closer look at the bars table.

id resource_type resource_id
1 "Foo" 1
2 "Baz" 1


Our SQL statement is trying to join the Baz with id 1 to a corresponding bar with resource_type “Baz” and resource_id 1. Well there it is, in the second row, with id 2. We don’t want that one, because we’re explicitly calling the method from the Bar with id 1. But the getter method for baz is not given that information.

We want a way to add AND `bars`.`id` = 1 to the query. Rails doesn’t currently provide a way to do this while retaining the ability to do the joins: the value 1 here is dependent on the instnace, and joins must construct table-referencing queries without references to instances. I do, but that post is for another time. Until then, there is another way around this, although it’s not the prettiest:

class Bar
  belongs_to :foo, -> { joins(:bar).where(bars: {resource_type: 'Foo'}) }, foreign_key: 'resource_id'
  belongs_to :baz, -> { joins(:bar).where(bars: {resource_type: 'Baz'}) }, foreign_

  def foo(*args)
    resource_type == "Foo" ? super : nil
  end

  def baz(*args)
    resource_type == "Baz" ? super : nil
  end
end

We simply override the :foo and :baz methods to immediately return nil if self does not have the corerct resource_type. It’s worth noting that this use of super only works because ActiveRecrd defines assocaition getter and setter methods in the module Bar::GeneratedAssociationMethods that gets included into Bar, rather than defining the methods on Bar itself. Go Rails!

Speaking of setter methods, there’s at least one more thing we have to add. It seems that Rails does not use the assocaition scopes on create, so if we use

bar.baz = Baz.first
bar
#  => #<Bar id: 1, resource_type: "Foo", resource_id: 1, foo_id: nil, deleted: false>

bar’s resource_type is still “Foo”! To solve this, we also override the setters in Bar, although this is beginning to feel a little hacky:

  def foo=(foo)
    super
    self.resource = foo
  end

  def bar=(bar)
    super
    self.resource = bar
  end
end

Note that there are many ways we could have done this, but this way gives us the following advantages:

  • super gets called first, which correctly raises an ActiveRecord::AssociationTypeMismatch exception if we pass in the wrong type before doing anything else to self.
  • self.resource=() sets the association cache for :resource for free while also setting self.resource_type, for a sligght usability advantage over simply using self.resource_type=()

Lastly, there are other methods also defined on assocaitions that we would need to override. Namely, build_foo, create_foo, and create_foo!. Rails is ponitferous. Let’s ust metaprogramming to make this a little more manageable:

class Bar < ActiveRecord::Base
  belongs_to :resource, polymorphic: true
  belongs_to :foo, ->{joins(:bars).where bars: {resource_type: "Foo"}}, foreign_key: :resource_id
  belongs_to :baz, ->{joins(:bars).where bars: {resource_type: "Baz"}}, foreign_key: :resource_id

  def foo(*args)
    resource_type == "Foo" ? super : nil
  end

  def baz(*args)
    resource_type == "Baz" ? super : nil
  end

  [:foo, :baz].each do |resource_name|
    ["#{resource_name}=", "build_#{resource_name}", "create_#{resource_name}", "create_#{resource_name}!"].each do |method|
      define_method(method) do |resource|
        typed_resource = super(resource)
        self.resource = typed_resource
      end
    end
  end

end

A little heavy. Not speaking softly anymore. What can I say, Rails is a bear.