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"
4endMigration
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
9endUsage
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
37endMigration
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
11endUsage
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 followingFriendships (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 }
32endHierarchical 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
38endMigration
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
9endUsage
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
17endUsing 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 hashClosure 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 hierarchiesPerformance 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
19endSelf-referential associations are essential for modeling real-world hierarchical relationships!
