Skip
Arish's avatar

27. Belongs To and Has One


Active Record Associations

Associations are connections between models. They make common operations simpler and easier by using the relationships between models.

Why Associations?

Without associations, you'd have to write a lot of manual code:

ruby
1# Without associations
2class Article < ApplicationRecord
3end
4
5class Comment < ApplicationRecord
6end
7
8# Getting all comments for an article
9comments = Comment.where(article_id: article.id)
10
11# Creating a comment for an article
12comment = Comment.create(article_id: article.id, body: "Great article!")

With associations, it becomes much cleaner:

ruby
1# With associations
2class Article < ApplicationRecord
3  has_many :comments
4end
5
6class Comment < ApplicationRecord
7  belongs_to :article
8end
9
10# Getting all comments
11comments = article.comments
12
13# Creating a comment
14article.comments.create(body: "Great article!")

belongs_to Association

belongs_to sets up a connection with another model where the current model contains the foreign key.

Basic Usage

ruby
1# The comments table has an article_id column
2class Comment < ApplicationRecord
3  belongs_to :article
4end
5
6# Migration
7class CreateComments < ActiveRecord::Migration[7.1]
8  def change
9    create_table :comments do |t|
10      t.text :body
11      t.references :article, null: false, foreign_key: true
12      t.timestamps
13    end
14  end
15end

Using belongs_to

ruby
1comment = Comment.first
2
3# Access the associated article
4comment.article            # => #<Article id: 1, title: "...">
5comment.article.title      # => "My Article"
6
7# Set the association
8comment.article = Article.find(2)
9comment.save
10
11# Build a new article (doesn't save)
12comment.build_article(title: "New Article")
13
14# Create a new article (saves immediately)
15comment.create_article(title: "New Article")

belongs_to Options

ruby
1class Comment < ApplicationRecord
2  # Basic
3  belongs_to :article
4  
5  # Custom class name
6  belongs_to :author, class_name: "User"
7  
8  # Custom foreign key
9  belongs_to :article, foreign_key: "post_id"
10  
11  # Optional association (allows nil)
12  belongs_to :category, optional: true
13  
14  # Polymorphic association
15  belongs_to :commentable, polymorphic: true
16  
17  # Touch parent on update
18  belongs_to :article, touch: true
19  
20  # Counter cache
21  belongs_to :article, counter_cache: true
22  # Requires articles.comments_count column
23  
24  # Inverse of (for bidirectional associations)
25  belongs_to :article, inverse_of: :comments
26  
27  # Dependent behavior
28  belongs_to :article, dependent: :destroy
29end

Optional belongs_to

By default in Rails 5+, belongs_to requires the associated record to exist:

ruby
1class Comment < ApplicationRecord
2  belongs_to :article  # article_id cannot be nil
3end
4
5comment = Comment.new(body: "Test")
6comment.valid?  # => false
7comment.errors  # => { article: ["must exist"] }
8
9# Make it optional
10class Comment < ApplicationRecord
11  belongs_to :article, optional: true
12end
13
14# Or globally in config
15# config/application.rb
16config.active_record.belongs_to_required_by_default = false

has_one Association

has_one sets up a one-to-one connection where the other model contains the foreign key.

Basic Usage

ruby
1class User < ApplicationRecord
2  has_one :profile
3end
4
5class Profile < ApplicationRecord
6  belongs_to :user
7end
8
9# Migration for profiles
10class CreateProfiles < ActiveRecord::Migration[7.1]
11  def change
12    create_table :profiles do |t|
13      t.string :bio
14      t.string :avatar_url
15      t.references :user, null: false, foreign_key: true
16      t.timestamps
17    end
18  end
19end

Using has_one

ruby
1user = User.first
2
3# Access the associated profile
4user.profile              # => #<Profile id: 1, ...>
5user.profile.bio          # => "Ruby developer"
6
7# Check if profile exists
8user.profile.present?     # => true
9
10# Set the association
11user.profile = Profile.new(bio: "New bio")
12user.save
13
14# Build a new profile (doesn't save)
15user.build_profile(bio: "My bio")
16
17# Create a new profile (saves immediately)
18user.create_profile(bio: "My bio")

has_one Options

ruby
1class User < ApplicationRecord
2  # Basic
3  has_one :profile
4  
5  # Custom class name
6  has_one :account, class_name: "UserAccount"
7  
8  # Custom foreign key
9  has_one :profile, foreign_key: "owner_id"
10  
11  # Dependent behavior
12  has_one :profile, dependent: :destroy
13  has_one :profile, dependent: :delete
14  has_one :profile, dependent: :nullify
15  
16  # Conditions
17  has_one :active_subscription, -> { where(active: true) }, class_name: "Subscription"
18  
19  # Through another association
20  has_one :account_history, through: :profile
21  
22  # Source (when through is used)
23  has_one :main_address, through: :profile, source: :address
24end

has_one vs belongs_to

The key difference is where the foreign key lives:

┌─────────────┐ ┌─────────────┐ │ User │ │ Profile │ ├─────────────┤ ├─────────────┤ │ id │◀────────│ user_id │ │ name │ has_one │ bio │ │ email │ │ avatar_url │ └─────────────┘ └─────────────┘ belongs_to

The model with the foreign key uses belongs_to.

Touch Option

Update parent's updated_at when child changes:

ruby
1class Comment < ApplicationRecord
2  belongs_to :article, touch: true
3end
4
5# When comment is saved/updated/destroyed:
6comment.save
7# Article's updated_at is also updated!
8
9# Touch specific column
10belongs_to :article, touch: :comments_updated_at

Counter Cache

Track the number of associated records efficiently:

ruby
1# Migration
2class AddCommentsCountToArticles < ActiveRecord::Migration[7.1]
3  def change
4    add_column :articles, :comments_count, :integer, default: 0
5    
6    # Backfill existing counts
7    Article.find_each do |article|
8      Article.reset_counters(article.id, :comments)
9    end
10  end
11end
12
13# Model
14class Comment < ApplicationRecord
15  belongs_to :article, counter_cache: true
16end
17
18# Usage
19article.comments_count  # No database query!
20
21# Custom column name
22belongs_to :article, counter_cache: :total_comments

Autosave

Automatically save associated records:

ruby
1class Article < ApplicationRecord
2  has_one :metadata, autosave: true
3end
4
5article = Article.find(1)
6article.metadata.description = "New description"
7article.save  # Also saves metadata!

These are the foundational one-to-one relationships in Rails!