RoR Associations

Associations define relationships between models. They tell Rails how records in different tables relate to each other. With associations, you can easily navigate from a user to their posts, from a post to its comments, or from an order to its items — all without writing complex queries.

The Core Concept: Related Records

Real World Relationship         Rails Association
-------------------------------------------------
One author writes many posts    User has_many :posts
Each post belongs to one author Post belongs_to :user
One post has many comments      Post has_many :comments
Each comment belongs to one post Comment belongs_to :post

belongs_to and has_many

These two always come as a pair. The model that holds the foreign key column uses belongs_to. The model on the "one" side uses has_many.

Database Tables:

users table:
  id | name  | email
  1  | Alice | alice@example.com
  2  | Bob   | bob@example.com

posts table:
  id | title        | body   | user_id
  1  | "Rails Intro" | "..."  | 1       ← belongs to user 1 (Alice)
  2  | "MVC Guide"   | "..."  | 1       ← belongs to user 1 (Alice)
  3  | "Ruby Tips"   | "..."  | 2       ← belongs to user 2 (Bob)

The user_id column in posts is the foreign key. It connects each post to a user.

Setting Up Associations in Models

app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

Setting Up the Foreign Key with a Migration

rails generate migration AddUserToPosts user:references

This generates:

class AddUserToPosts < ActiveRecord::Migration[7.1]
  def change
    add_reference :posts, :user, null: false, foreign_key: true
  end
end

Run rails db:migrate and the user_id column appears in the posts table.

Using Associations in Code

alice = User.find(1)

# Get all posts written by Alice
alice.posts
# SQL: SELECT * FROM posts WHERE user_id = 1

# Get the count
alice.posts.count
# => 2

# Create a post that already belongs to Alice
alice.posts.create(title: "New Post", body: "Content here")
# Automatically sets user_id = 1

# Get the author of a post
post = Post.find(1)
post.user
# => Returns the User object for Alice

post.user.name
# => "Alice"

Association Diagram

User (id: 1, name: "Alice")
        |
        | has_many
        |
        +-- Post (id: 1, user_id: 1, title: "Rails Intro")
        |         |
        |         | has_many
        |         |
        |         +-- Comment (id: 1, post_id: 1, body: "Great!")
        |         +-- Comment (id: 2, post_id: 1, body: "Thanks!")
        |
        +-- Post (id: 2, user_id: 1, title: "MVC Guide")

has_many :through

Use has_many :through when two models connect through a third model (a join table).

Example: Doctors have many Patients through Appointments

class Doctor < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end

class Appointment < ApplicationRecord
  belongs_to :doctor
  belongs_to :patient
end

class Patient < ApplicationRecord
  has_many :appointments
  has_many :doctors, through: :appointments
end
Database Tables:

doctors:      id | name
patients:     id | name
appointments: id | doctor_id | patient_id | date

Usage:
doctor = Doctor.find(1)
doctor.patients   ← all patients this doctor has seen
patient.doctors   ← all doctors this patient has visited

has_one

Use has_one when a record has exactly one related record:

class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user
end

user = User.find(1)
user.profile           ← the user's single profile
user.profile.bio       ← access the profile's bio
user.create_profile(bio: "Developer from NYC")

The dependent Option

Control what happens to child records when the parent is deleted:

has_many :posts, dependent: :destroy   ← deletes all posts when user is deleted
has_many :posts, dependent: :nullify   ← sets user_id to NULL (posts survive)
has_many :posts, dependent: :restrict_with_error  ← blocks deletion if posts exist

Always set dependent to avoid orphaned records floating in your database.

Querying Through Associations

# Find all posts for user 1
Post.where(user_id: 1)

# Better — use the association
user = User.find(1)
user.posts

# Filter through the association
user.posts.where(published: true)
user.posts.order(created_at: :desc)
user.posts.limit(5)

# Does Alice have any posts?
user.posts.any?    # true or false
user.posts.empty?  # true or false

Association Validations

By default, belongs_to requires the parent to exist. A post cannot be saved without a valid user_id.

post = Post.new(title: "Orphan Post")
post.save
# => false
# Error: "User must exist"

To make the association optional:

belongs_to :user, optional: true

Associations remove the need to manually manage foreign keys and write JOIN queries. They let your code read like plain English: user.posts, post.user, doctor.patients.

Leave a Comment

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