Identifying Foreign Key Dependencies from ActiveRecord::Base Classes
Ryan Stenberg, Former Developer
Article Category:
Posted on
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!