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.
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_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.
Again, simple, with one caveat: It belongs to a
:user and to a
:followed_user through the
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:
class Following < ApplicationRecord validate :realism private def realism return unless user_id == followed_user_id errors.add :user, 'Only a solipsist would follow themselves.' end end
User Model/Following Concern
I separated the
User model code into a concern given the amount of helper methods I wrote (
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
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
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
@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