Skip
Arish's avatar

31. Self-Referential Associations


Self-Referential Associations

Self-referential associations are when a model has a relationship with itself. Common examples include employees and managers, followers on social networks, and parent-child categories.

Basic Self-Referential belongs_to

Employee and Manager

ruby
1class Employee < ApplicationRecord
2  belongs_to :manager, class_name: "Employee", optional: true
3  has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
4end

Migration

ruby
1class CreateEmployees < ActiveRecord::Migration[7.1]
2  def change
3    create_table :employees do |t|
4      t.string :name
5      t.references :manager, foreign_key: { to_table: :employees }
6      t.timestamps
7    end
8  end
9end

Usage

ruby
1# Create employees
2ceo = Employee.create(name: "Jane CEO")
3manager = Employee.create(name: "Bob Manager", manager: ceo)
4developer = Employee.create(name: "Alice Developer", manager: manager)
5
6# Navigate the hierarchy
7developer.manager              # => Bob Manager
8developer.manager.manager      # => Jane CEO
9manager.subordinates           # => [Alice Developer]
10ceo.subordinates               # => [Bob Manager]
11
12# Find all without manager (top level)
13Employee.where(manager: nil)

Many-to-Many Self-Referential

Social Network Followers

ruby
1class User < ApplicationRecord
2  # People I follow
3  has_many :active_follows, class_name: "Follow",
4           foreign_key: "follower_id", dependent: :destroy
5  has_many :following, through: :active_follows, source: :followed
6  
7  # People who follow me
8  has_many :passive_follows, class_name: "Follow",
9           foreign_key: "followed_id", dependent: :destroy
10  has_many :followers, through: :passive_follows, source: :follower
11  
12  def follow(other_user)
13    following << other_user unless self == other_user
14  end
15  
16  def unfollow(other_user)
17    following.delete(other_user)
18  end
19  
20  def following?(other_user)
21    following.include?(other_user)
22  end
23end
24
25class Follow < ApplicationRecord
26  belongs_to :follower, class_name: "User"
27  belongs_to :followed, class_name: "User"
28  
29  validates :follower_id, uniqueness: { scope: :followed_id }
30  validate :cannot_follow_self
31  
32  private
33  
34  def cannot_follow_self
35    errors.add(:base, "You can't follow yourself") if follower_id == followed_id
36  end
37end

Migration

ruby
1class CreateFollows < ActiveRecord::Migration[7.1]
2  def change
3    create_table :follows do |t|
4      t.references :follower, null: false, foreign_key: { to_table: :users }
5      t.references :followed, null: false, foreign_key: { to_table: :users }
6      t.timestamps
7    end
8    
9    add_index :follows, [:follower_id, :followed_id], unique: true
10  end
11end

Usage

ruby
1alice = User.find(1)
2bob = User.find(2)
3
4alice.follow(bob)
5alice.following?(bob)     # => true
6bob.followers             # => [alice]
7alice.following           # => [bob]
8alice.followers.count     # Number of followers
9alice.following.count     # Number following

Friendships (Bidirectional)

ruby
1class User < ApplicationRecord
2  has_many :friendships, dependent: :destroy
3  has_many :friends, through: :friendships
4  
5  has_many :inverse_friendships, class_name: "Friendship",
6           foreign_key: "friend_id", dependent: :destroy
7  has_many :inverse_friends, through: :inverse_friendships, source: :user
8  
9  def all_friends
10    friends + inverse_friends
11  end
12  
13  def friend?(user)
14    friends.include?(user) || inverse_friends.include?(user)
15  end
16  
17  def befriend(user)
18    friendships.create(friend: user) unless friend?(user)
19  end
20  
21  def unfriend(user)
22    friendships.where(friend: user).destroy_all
23    inverse_friendships.where(user: user).destroy_all
24  end
25end
26
27class Friendship < ApplicationRecord
28  belongs_to :user
29  belongs_to :friend, class_name: "User"
30  
31  validates :friend_id, uniqueness: { scope: :user_id }
32end

Hierarchical Data (Categories)

Basic Parent-Child

ruby
1class Category < ApplicationRecord
2  belongs_to :parent, class_name: "Category", optional: true
3  has_many :children, class_name: "Category", foreign_key: "parent_id"
4  
5  scope :roots, -> { where(parent_id: nil) }
6  
7  def ancestors
8    node, nodes = self, []
9    while node = node.parent
10      nodes.unshift(node)
11    end
12    nodes
13  end
14  
15  def descendants
16    children.flat_map { |child| [child] + child.descendants }
17  end
18  
19  def self_and_ancestors
20    [self] + ancestors
21  end
22  
23  def self_and_descendants
24    [self] + descendants
25  end
26  
27  def root?
28    parent_id.nil?
29  end
30  
31  def leaf?
32    children.empty?
33  end
34  
35  def depth
36    ancestors.count
37  end
38end

Migration

ruby
1class CreateCategories < ActiveRecord::Migration[7.1]
2  def change
3    create_table :categories do |t|
4      t.string :name
5      t.references :parent, foreign_key: { to_table: :categories }
6      t.timestamps
7    end
8  end
9end

Usage

ruby
1# Create category tree
2electronics = Category.create(name: "Electronics")
3computers = Category.create(name: "Computers", parent: electronics)
4laptops = Category.create(name: "Laptops", parent: computers)
5
6# Navigate
7laptops.parent              # => Computers
8laptops.ancestors           # => [Electronics, Computers]
9electronics.children        # => [Computers]
10electronics.descendants     # => [Computers, Laptops]
11Category.roots              # => [Electronics, ...]

Comment Threads (Nested Comments)

ruby
1class Comment < ApplicationRecord
2  belongs_to :commentable, polymorphic: true
3  belongs_to :parent, class_name: "Comment", optional: true
4  has_many :replies, class_name: "Comment", foreign_key: "parent_id"
5  
6  scope :root_comments, -> { where(parent_id: nil) }
7  
8  def depth
9    parent ? parent.depth + 1 : 0
10  end
11  
12  def thread
13    root = self
14    root = root.parent while root.parent
15    root
16  end
17end

Using Gems for Trees

For complex hierarchies, consider using gems:

Ancestry Gem

ruby
1# Gemfile
2gem 'ancestry'
3
4# Migration
5class AddAncestryToCategories < ActiveRecord::Migration[7.1]
6  def change
7    add_column :categories, :ancestry, :string
8    add_index :categories, :ancestry
9  end
10end
11
12# Model
13class Category < ApplicationRecord
14  has_ancestry
15end
16
17# Usage
18category.parent
19category.children
20category.ancestors
21category.descendants
22category.subtree
23category.depth
24Category.roots
25Category.arrange  # Returns nested hash

Closure Tree Gem

ruby
1# Gemfile
2gem 'closure_tree'
3
4# Model
5class Category < ApplicationRecord
6  has_closure_tree
7end
8
9# More efficient queries for deep hierarchies

Performance Considerations

ruby
1# Avoid N+1 with recursive queries
2class Category < ApplicationRecord
3  # Use includes for immediate children
4  scope :with_children, -> { includes(:children) }
5  
6  # For PostgreSQL, use recursive CTE
7  def self.descendants_of(category_id)
8    sql = <<-SQL
9      WITH RECURSIVE tree AS (
10        SELECT * FROM categories WHERE id = #{category_id}
11        UNION ALL
12        SELECT c.* FROM categories c
13        JOIN tree t ON c.parent_id = t.id
14      )
15      SELECT * FROM tree WHERE id != #{category_id}
16    SQL
17    find_by_sql(sql)
18  end
19end

Self-referential associations are essential for modeling real-world hierarchical relationships!