Self-Referential “has_many :through” Relationships in Rails 5

in code


A self-referential has_many :through relationship is one where a class interacts with itself through a join table. The classic use are followings:

  • I follow another user.
  • Another user follows me.

So a Following is a two-way relationship between the User class and itself. There are a bunch of guides for this that are either outdated or overly-complex. In that light, here is the simplest route to a self-referential relationship in Rails 5:

Followings Database Migrations

There’s an actual :users table, but the :followed_users doesn’t exist. I chose to use a verbose name for future readability.

class AddFollowings  < ActiveRecord::Migration[5.0]
  def change
    create_table :followings do |t|
      t.timestamps
      t.belongs_to :user
      t.belongs_to :followed_user
      t.index [:user_id, :followed_user_id], unique: true
    end
  end
end

The original table for this was a simple join, but experience showed that we needed both a primary key and timestamp fields. In Rails 5, a has_and_belongs_to_many or has_many :through only work if you don’t want to attach any extra functionality to the model. For us we needed to both track counter cache columns and (for analytics) see when one user followed another.

Following Model

Again, simple, with one caveat: It belongs to a :user and to a :followed_user through the User class.

class Following < ApplicationRecord
  belongs_to :user
  belongs_to :followed_user, class_name: 'User'
end

The uniqueness (index) constraint on the join table will stop one user from following another more than once, but it won’t prevent a solipsistic self-following:

  validate :realism

  private

  def realism
    return unless user_id == followed_user_id
    errors.add :user, 'Only a solipsist would follow themselves.'
  end

User Model/Following Concern

I separated the User model code into a concern given the amount of helper methods I wrote (follow, followed_by?, etc). This permitted me to neatly encapsulate functionality and testing. Here is my solution with other callbacks and methods removed:

class User < ApplicationRecord
  has_many :followings
  has_many :followed_users, through: :followings

  has_many :followers, foreign_key: :followed_user_id, class_name: 'Following'
  has_many :follower_users, through: :followers, source: :user
end

Counter Cache

Hark back to the above Following model. Who doesn’t want counter cache columns for following and follower users?

class Following < ApplicationRecord
  belongs_to :user, touch: true, counter_cache: true
  belongs_to :followed_user, counter_cache: :followers_count, class_name: 'User'
end

This setup will change followings_count and followers_count on create and destroy:

SQL (0.4ms)  UPDATE `users` SET `followings_count` = COALESCE(`followings_count`, 0) - 1 WHERE `users`.`id` = 1
SQL (0.4ms)  UPDATE `users` SET `followers_count` = COALESCE(`followers_count`, 0) - 1 WHERE `users`.`id` = 8

Useage

@user.follower_users
@user.followed_users

@user.followed_users << @other_user
@other_user.follower_users << @user

@user.followed_users.delete @other_user
@other_user.follower_users.delete @user


Remap a JavaScript Object

in code

Replace Odd Numbers of Spaces Only

in code

Coffee is Good

in code


Your email address will not be published. Required fields are marked *