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!