Identifying Foreign Key Dependencies from ActiveRecord::Base Classes

Ever find yourself in a situation where you were given an ActiveRecord model and you wanted to figure out all the models it had a foreign key dependency (belongs_to association) with? Well, I had to do just that in some recent sprig-reap work. Given the class for a model, I needed to find all the class names for its belongs_to associations.

In order to figure this out, there were a few steps I needed to take..

Identify the Foreign Keys / belongs_to Associations

ActiveRecord::Base-inherited classes (models) provide a nice interface for inspecting associations -- the reflect_on_all_associations method. In my case, I was looking specifically for belongs_to associations. I was in luck! The method takes an optional argument for the kind of association. Here's an example:

Post.reflect_on_all_associations(:belongs_to)
# => array of ActiveRecord::Reflection::AssociationReflection objects

Once I had a list of all the belongs_to associations, I needed to then figure out what the corresponding class names were.

Identify the Class Name from the Associations

When dealing with ActiveRecord::Reflection::AssociationReflection objects, there are two places where class names can be found. These class names are downcased symbols of the actual class. Here are examples of how to grab a class name from both a normal belongs_to association and one with an explicit class_name.

Normal belongs_to:

class Post < ActiveRecord::Base
  belongs_to :user
end

association = Post.reflect_on_all_associations(:belongs_to).first
# => ActiveRecord::Reflection::AssociationReflection instance

name = association.name
# => :user

With an explicit class_name:

class Post < ActiveRecord::Base
  belongs_to :creator, class_name: 'User'
end

association = Post.reflect_on_all_associations(:belongs_to).first
# => ActiveRecord::Reflection::AssociationReflection instance

name = association.options[:class_name]
# => 'User'

Getting the actual class:

ActiveRecord associations have a build in klass method that will return the actual class based on the appropriate class name:

Post.reflect_on_all_associations(:belongs_to).first.klass
# => User

Handle Polymorphic Associations

Polymorphism is tricky. When dealing with a polymorphic association, you have a single identifier. Calling association.name would return something like :commentable. In a polymorphic association, we're probably looking to get back multiple class names -- like Post and Status for example.

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
  has_many :comments, as: :commentable
end
class Status < ActiveRecord::Base
  has_many :comments, as: :commentable
end
association = Comment.reflect_on_all_associations(:belongs_to).first
# => ActiveRecord::Reflection::AssociationReflection instance
polymorphic = association.options[:polymorphic]
# => true
associations = ActiveRecord::Base.subclasses.select do |model|
  model.reflect_on_all_associations(:has_many).any? do |has_many_association|
    has_many_association.options[:as] == association.name
  end
end
# => [Post, Status]

Polymorphic?

To break down the above example, association.options[:polymorphic] gives us true if our association is polymorphic and nil if it isn't.

Models with Polymorphic has_many Associations

If we know an association is polymorphic, the next step is to check all the models (ActiveRecord::Base.subclasses, could also do .descendants depending on how you want to handle subclasses of subclasses) that have a matching has_many polymorphic association (has_many_association.options[:as] == association.name from the example). When there's a match on a has_many association, you know that model is one of the polymorphic belongs_to associations!

Holistic Dependency Finder

As an illustration of how I handled my dependency sleuthing -- covering all the cases -- here's a class I made that takes a belongs_to association and provides a nice interface for returning all its dependencies (via its dependencies method):

class Association < Struct.new(:association)
  delegate :foreign_key, to: :association
  
  def klass
    association.klass unless polymorphic?
  end
 
  def name
    association.options[:class_name] || association.name
  end
 
  def polymorphic?
    !!association.options[:polymorphic]
  end
 
  def polymorphic_dependencies
    return [] unless polymorphic?
    @polymorphic_dependencies ||= ActiveRecord::Base.subclasses.select { |model| polymorphic_match? model }
  end
 
  def polymorphic_match?(model)
    model.reflect_on_all_associations(:has_many).any? do |has_many_association|
      has_many_association.options[:as] == association.name
    end
  end
 
  def dependencies
    polymorphic? ? polymorphic_dependencies : Array(klass)
  end
 
  def polymorphic_type
    association.foreign_type if polymorphic?
  end
end

Here's a full example with the Association class in action:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
  belongs_to :creator, class_name: 'User'
  has_many :comments, as: :commentable
end
class Status < ActiveRecord::Base
  belongs_to :user
  has_many :comments, as: :commentable
end
class User < ActiveRecord::Base
  has_many :posts
  has_many :statuses
end
Association.new(Comment.reflect_on_all_associations(:belongs_to).first).dependencies
# => [Post, Status]
Association.new(Post.reflect_on_all_associations(:belongs_to).first).dependencies
# => [User]
Association.new(Status.reflect_on_all_associations(:belongs_to).first).dependencies
# => [User]

The object-oriented approach cleanly handles all the cases for us! Hopefully this post has added a few tricks to your repertoire. Next time you find yourself faced with a similar problem, use reflect_on_all_associations for great justice!

Ryan is a developer in Viget's Falls Church, VA, HQ, where he believes in being a liason for both the technical and non-technical. He builds elegant tools for clients such as Bozzuto and Millitello Capital—as well as internal tools that we use at Viget every day.

More posts by Ryan