Search code examples
ruby-on-railsactiverecordsingle-table-inheritance

Use `becomes` on a ActiveRecord::Relation (STI)


# Models
class Animal < ActiveRecord::Base
  belongs_to :zoo
end

class Dog < Animal
  belongs_to :leg
end

class Snake < Animal
end

class Leg < ActiveRecord::Base
end

class Zoo
  has_many :animals
end

# Test, which fails with
# Association named 'leg' was not found on Animal; perhaps you misspelled it?
Zoo.first.animals.
         .where(:type => 'Dog')
         .includes(:leg)

In this example, Rails can not know the specific type of the objects queried (it would have to analyze the where statement for that, which it does not appear to do). Therefore, it fails, as the association is not defined on the generic model Animal, but on the Dog model.

Is there a way of specifying the type of the objects about to be retrieved, so that the example works?


Solution

  • Here's a solution that looks inefficient at first, but is actually very performant due to the way ActiveRecord builds up its query object.

    A good solution to your query is:

    Dog.where(id: Zoo.first.animals.where(type: 'Dog').select(:id))
       .includes(:leg)
    

    At first glance, it looks like it should execute 2 queries, but ActiveRecord is smart enough to produce the following SQL in a single DB call:

    SELECT "animals".*
    FROM "animals"
    WHERE "animals"."id" IN (
      SELECT "animals"."id"
      FROM "animals"
      WHERE "animals"."id" = 111
        AND "animals"."type" = 'Dog'
    )
    

    Since the outer query is driven by the Dog class, then the .includes will work. And so this is a generic way of behaving like the becomes method on an ActiveRecord::Relation.

    Note: The .select(:id) part of the code isn't actually required. I just included that for clarity, to show how AR uses that part of the query.