Insights into identifying Polymorphic Relationships in Rails

Published: October 08, 2018

While coding the backend for my capstone FamNet, an open sourced closed social network for families, I eventually needed to create a way for users to be notified about new content where they've interacted with my primary models: Events, EventRsvps, Posts, Comments, Comment on Comments, and Recipes. In the end, the most complicated was notifications since it was tied to almost every single model and had to process a lot of different sources of information to decide who gets notified about a model's content. There were two ways I could do this:

I could approach it from a has_many :X, through: Y which would create a lot of tables for each interaction type along with a lot of potential inheritance issues. Or, I could approach it using a polymorphic self-relationship table, and here is why:

Pros

  • It's self-contained and can be included in any model very easily.
  • It eliminates having duplicate models using a has_many :X, through: Y method.
  • It follows a Composition over inheritance pattern.

Cons

  • It can be tricky to render or search sometimes.
  • It can be tricky to debug sometimes.

In my opinion, the pros outweigh the cons which lead me to implement a few polymorphic classes to solve my app's design needs. The primary 4 types of questions: Does a sibling content model need to be notified; Does a child content model need to be notified; Does a parent content model need to be notified; and, has the notifyee already been notified about this action? (i.e. the owner of a top level post, comments on his own post. If another person comments, that owner doesn't need two notifications.)

So, it all starts with a callback: after_commit :notify_members, unless: Proc.new { self.class == Member } There are four primary models in my project Post, Member, Recipe, and Event. These all have supporting models such as Comment, CommentReply (child of Comment), and EventRsvp to name a few.

That results in the next problem, who's the parent and who's the child class? That was initially a tricky pattern to figure out until I learned about the detect method which helped me out a lot. Additionally, I also had to figure out which of those detected classes were polymorphic.

That led me to take a multi-step approach initially identifying the models then determining which were polymorphic.

  def notify_members
    @parent_klass = [Post, Event, Recipe].detect { |i| self.class == i }
    @child_klass = [Comment, CommentReply, Reaction, EventRsvp].detect { |i| self.class == i }

    # This is the object storing a string of the polymorphic klass for member lookup. (i.e. if the target is Comment then the attribute will be "commentable".)
    @target_attribute_polymorphic_klass = get_polymorphic_klass(self)
    ...
  end
  # This method gets the polymorphic class attribute from the active record passed to it. 
  def get_polymorphic_klass(target)
    # It cycles through each key looking for a match to `_type` and returns it once found.
    target.attributes.each_key do |i|
     return i.slice(0..-6) if i.match?(/[A-z]+_type/)
    end
  end

Once, I had @parent_klass, @child_klass, and @target_attribute_polymorphic_klass it was easy to build the helper methods and conditional logic trees to figure out when and to whom to send a notification.

If you want to see more of the code, you can view it here or view the FamNet Project here.