- Active Record associations define relationships between models.
- Associations are set up using macro-style calls like
has_many
,belongs_to
, etc. - Rails handles primary and foreign key relationships between models automatically.
- This simplifies common operations, improves readability, and helps in managing related data easily.
-
Define relationships between models.
-
Implemented as macro-style calls (e.g.,
has_many
:comments
). -
Helps manage data effectively, making operations simpler.
-
Rails defines & manages Primary Key (PK) and Foreign Key (FK) relationships.
-
Provides useful methods for working with related data.
- Migration for Authors & Books
class CreateAuthors < ActiveRecord::Migration[8.0]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.references :author
t.datetime :published_at
t.timestamps
end
end
end
- Models Without Associations
class Author < ApplicationRecord
end
class Book < ApplicationRecord
end
- Creating a Book (Manual FK Assignment)
@book = Book.create(author_id: @author.id, published_at: Time.now)
Deleting an Author & Their Books (Manual Cleanup)
@books = Book.where(author_id: @author.id)
@books.each do |book|
book.destroy
end
@author.destroy
- Updated Models with Associations
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author
end
- Creating a Book (Simplified)
@book = @author.books.create(published_at: Time.now)
- Deleting an Author & Their Books (Automated Cleanup)
@author.destroy
- Migration for Foreign Key
rails generate migration AddAuthorToBooks author:references
-
Adds
author_id
column. -
Sets up FK relationship in the database.
- Rails supports six types of associations, each serving a specific purpose:
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many
- A
belongs_to
association sets up a relationship where each instance of the declaring model "belongs to" one instance of another model. Example:
class Book < ApplicationRecord
belongs_to :author
end
- Migration Example:
class CreateBooks < ActiveRecord::Migration[8.0]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
-
The
belongs_to
association ensures a reference column exists in the model's table. -
Using
optional: true
allows the foreign key to beNULL
.
class Book < ApplicationRecord
belongs_to :author, optional: true
end
- Adding a foreign key constraint ensures integrity:
create_table :books do |t|
t.belongs_to :author, foreign_key: true
# ...
end
- When you declare a
belongs_to
association, the model gains these methods:
Retrieving the Association
@author = @book.author
- Force a database reload:
@author = @book.reload_author
- Reset cached association:
@book.reset_author
Assigning the Association
@book.author = @author
- Build an association (not saved):
@author = @book.build_author(name: "John Doe")
- Create and save an association:
@author = @book.create_author(name: "John Doe")
- Create and save, raising an error if invalid:
@book.create_author!(name: "") # Raises ActiveRecord::RecordInvalid
Checking for Association Changes
@book.author_changed? # => true if association is updated
@book.author_previously_changed? # => true if previously updated
Checking for Existing Associations
if @book.author.nil?
@msg = "No author found for this book"
end
Saving Behavior
-
Assigning an object to a belongs_to association does not automatically save either the parent or child.
-
However, saving the parent object does save the association:
@book.save
- A
has_one
association indicates that one other model has a reference to this model. That model can be fetched through this association.
class Supplier < ApplicationRecord
has_one :account
end
class CreateSuppliers < ActiveRecord::Migration[8.0]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true
t.string :account_number
t.timestamps
end
end
end
Methods Added by has_one
- When declaring a
has_one
association, Rails automatically provides the following methods:
association
association=
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
@supplier.account = @account
@supplier.build_account(terms: "Net 30")
@supplier.create_account(terms: "Net 30")
Checking for Existing Associations
if @supplier.account.nil?
@msg = "No account found for this supplier"
end
Saving Behavior
-
When assigning an object to a
has_one
association, it is automatically saved unlessautosave: false
is used. If the parent object is new, the child objects are saved when the parent is saved. -
Use
build_association
to work with an unsaved object before saving it explicitly.
- A
has_many
association creates a one-to-many relationship where one model can be associated with multiple records of another model.
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
class CreateAuthors < ActiveRecord::Migration[6.0]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
end
end
Methods Added by has_many
collection
collection<<
collection.delete
collection.destroy
collection=, collection.clear
collection.empty?
collection.size
collection.count
collection.build(attributes = {})
collection.create(attributes = {})
collection.reload
- A
has_and_belongs_to_many
association creates a directmany-to-many
relationship between two models without using a separate model for the join table.
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
class CreateJoinTableUsersGroups < ActiveRecord::Migration[6.0]
def change
create_join_table :users, :groups do |t|
t.index [:user_id, :group_id]
t.index [:group_id, :user_id]
end
end
end
Methods Added by has_and_belongs_to_many
collection
collection<<
collection.delete
collection.destroy
collection=, collection.clear
collection.empty?, collection.size, collection.count
collection.build(attributes = {})
collection.create(attributes = {})
collection.reload
- This type of association is best used when a simple join table is sufficient without needing extra attributes in the join table. If more attributes are needed, a
has_many :through
association is recommended.
- Polymorphic associations allow a model to belong to multiple other models through a single association. This is useful when a model needs to be linked to different types of models.
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
class CreatePictures < ActiveRecord::Migration[8.0]
def change
create_table :pictures do |t|
t.string :name
t.belongs_to :imageable, polymorphic: true
t.timestamps
end
end
end
-
Rails can infer primary key-foreign key relationships, but when using composite primary keys, Rails defaults to the id column unless explicitly specified.
-
Refer to the Composite Primary Keys guide for details on handling associations with composite keys in Rails.
- A self-join is when a table joins itself, commonly used for hierarchical relationships like employees and managers.
class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee", optional: true
end
class CreateEmployees < ActiveRecord::Migration[8.0]
def change
create_table :employees do |t|
t.belongs_to :manager, foreign_key: { to_table: :employees }
t.timestamps
end
end
end
employee = Employee.find(1)
subordinates = employee.subordinates
manager = employee.manager
Single Table Inheritance (STI)
allows multiple models to be stored in a single database table. This is useful when different entities share common attributes and behavior but also have specific behaviors.
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
- The type field is crucial as it differentiates between models (e.g., Car, Motorcycle, Bicycle).
$ bin/rails generate model car --parent=Vehicle
- Generates a model that inherits from Vehicle without creating a separate table.
class Car < Vehicle
end
- This allows Car to use all behaviors and attributes of Vehicle.
Car.create(color: "Red", price: 10000)
# SQL Generated:
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
Car.all
# SQL Generated:
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
class Car < Vehicle
def honk
"Beep Beep"
end
end
car = Car.first
car.honk # => 'Beep Beep'
- Each model can have its own controller:
class CarsController < ApplicationController
def index
@cars = Car.all
end
end
- If using a different column name (e.g., kind instead of type):
class Vehicle < ApplicationRecord
self.inheritance_column = "kind"
end
- To treat type as a normal column:
class Vehicle < ApplicationRecord
self.inheritance_column = nil
end
Vehicle.create!(type: "Car") # Treated as a normal attribute
- Delegated types solve the Single Table Inheritance (STI) issue of table bloat by allowing shared attributes to be stored in a superclass table while subclass-specific attributes remain in separate tables.
-
A superclass stores shared attributes.
-
Subclasses inherit from the superclass and have separate tables for additional attributes.
-
Prevents unnecessary attribute sharing across all subclasses.
- Run the following commands to generate models:
$ bin/rails generate model entry entryable_type:string entryable_id:integer
$ bin/rails generate model message subject:string body:string
$ bin/rails generate model comment content:string
class Entry < ApplicationRecord
end
class Message < ApplicationRecord
end
class Comment < ApplicationRecord
end
- Define
delegated_type
in the superclass:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end
-
entryable_type
stores the subclass name. -
entryable_id
stores the subclass record ID.
- Create a module to associate subclasses:
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
- Include it in subclass models:
class Message < ApplicationRecord
include Entryable
end
class Comment < ApplicationRecord
include Entryable
end
Method | Returns |
---|---|
Entry.entryable_types |
["Message", "Comment"] |
Entry#entryable_class |
Message or Comment |
Entry#entryable_name |
"message" or "comment" |
Entry.messages |
Entry.where(entryable_type: "Message") |
Entry.comments |
Entry.where(entryable_type: "Comment") |
Entry#message? |
true if entryable_type == "Message" |
Entry#comment? |
true if entryable_type == "Comment" |
Entry#message |
Message record if entryable_type == "Message" , else nil |
Entry#comment |
Comment record if entryable_type == "Comment" , else nil |
- Create an Entry with a subclass object:
Entry.create! entryable: Message.new(subject: "hello!")
- Delegate methods to subclasses:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ]
delegate :title, to: :entryable
end
class Message < ApplicationRecord
include Entryable
def title
subject
end
end
class Comment < ApplicationRecord
include Entryable
def title
content.truncate(20)
end
end
- This allows
Entry#title
to call subject for Message and a truncated content for Comment.
- Active Record associations use caching to store loaded data.
author.books.load # Loads books from DB
author.books.size # Uses cached books
author.books.empty? # Uses cached books
- Use
.reload
to refresh data from the database:
author.books.reload.empty?
- Avoid naming associations using reserved
ActiveRecord::Base
methods like attributes or connection to prevent conflicts.
- Associations do not modify the database schema; migrations must be created manually.
class AddAuthorToBooks < ActiveRecord::Migration[8.0]
def change
add_reference :books, :author
end
end
-
Default join table follows lexical ordering (e.g., authors_books).
-
Example migration without a primary key:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[8.0]
def change
create_table :assemblies_parts, id: false do |t|
t.bigint :assembly_id
t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
add_index :assemblies_parts, :part_id
end
end
- Alternatively, use
create_join_table
:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[8.0]
def change
create_join_table :assemblies, :parts do |t|
t.index :assembly_id
t.index :part_id
end
end
end
- Requires an
id
column.
class CreateAppointments < ActiveRecord::Migration[8.0]
def change
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
- Associations within the same module work automatically:
module MyApplication::Business
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
end
- If models exist in different modules, specify
class_name
explicitly:
module MyApplication::Business
class Supplier < ApplicationRecord
has_one :account, class_name: "MyApplication::Billing::Account"
end
end
module MyApplication::Billing
class Account < ApplicationRecord
belongs_to :supplier, class_name: "MyApplication::Business::Supplier"
end
end
- Rails detects bi-directional associations automatically:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
- Benefits:
- Prevents unnecessary queries.
- Ensures data consistency.
- Auto-saves associated records.
- Validates presence of associations.
- Active Record does not auto-detect bi-directional relationships when custom foreign keys are used:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :writer, class_name: "Author", foreign_key: "author_id"
end
-
This may cause extra queries and inconsistent data.
-
Solution: Use
inverse_of
to explicitly declare bi-directionality:
class Author < ApplicationRecord
has_many :books, inverse_of: "writer"
end
- Rails provides several options to customize association behavior.
- Specifies the actual model name if it differs from the association name.
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron"
end
-
Controls what happens to associated objects when the owner is destroyed:
:destroy
- Calls destroy on associated objects, executing callbacks.:delete
– Deletes associated records directly, bypassing callbacks.:destroy_async
– Asynchronously enqueues a job to destroy associated objects.:nullify
– Sets the foreign key to NULL.:restrict_with_exception
– Raises ActiveRecord::DeleteRestrictionError if there are associated records.:restrict_with_error
– Adds an error to the owner if there are associated objects.
Notes:
-
Should not be used on
belongs_to
with ahas_many
association. -
Ignored when using
:through
. -
Does not work on
has_and_belongs_to_many
.
- Specifies a custom foreign key column name.
class Supplier < ApplicationRecord
has_one :account, foreign_key: "supp_id"
end
- Sets a different primary key for associations.
class User < ApplicationRecord
self.primary_key = "guid"
end
class Todo < ApplicationRecord
belongs_to :user, primary_key: "guid"
end
- Updates the associated object's
updated_at
timestamp when this object is saved or destroyed.
class Book < ApplicationRecord
belongs_to :author, touch: true
end
-
If
true
, new associated objects are validated when saving the parent object. -
Default:
false
.
- Specifies the inverse association name.
class Supplier < ApplicationRecord
has_one :account, inverse_of: :supplier
end
class Account < ApplicationRecord
belongs_to :supplier, inverse_of: :account
end
- Used in has_many :through with polymorphic associations.
class Author < ApplicationRecord
has_many :books
has_many :paperbacks, through: :books, source: :format, source_type: "Paperback"
end
- Ensures strict loading whenever an associated record is accessed.
- Defines the foreign key column in a
has_and_belongs_to_many
join table.
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
- Specifies the custom join table name in
has_and_belongs_to_many
associations.
- Scopes allow defining reusable queries as method calls on associations.
where
- Used to filter associated records.
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { where factory: "Seattle" }
end
- Using a hash automatically scopes record creation:
@parts.assemblies.create # Ensures factory is 'Seattle'
includes
- Used for eager-loading nested associations.
class Supplier < ApplicationRecord
has_one :account, -> { includes :representative }
end
- Not needed for immediate associations.
readonly
- Makes associated objects read-only.
class Book < ApplicationRecord
belongs_to :author, -> { readonly }
end
- Prevents modifications:
@book.author.save! # Raises ActiveRecord::ReadOnlyRecord error
select
- Restricts selected columns.
class Author < ApplicationRecord
has_many :books, -> { select(:id, :title) }
end
- For
belongs_to
,specify :foreign_key
:
class Book < ApplicationRecord
belongs_to :author, -> { select(:id, :name) }, foreign_key: "author_id"
end
- Used for
has_many
andhas_and_belongs_to_many
associations.
group
- Groups records based on an attribute.
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
limit
- Restricts the number of records.
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { order("created_at DESC").limit(50) }
end
order
- Specifies order of records.
class Author < ApplicationRecord
has_many :books, -> { order "date_confirmed DESC" }
end
distinct
- Ensures uniqueness in associated records.
class Person < ApplicationRecord
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
- Prevent duplicates in the database:
add_index :readings, [:person_id, :article_id], unique: true
- Avoid using
include?
for uniqueness (race condition risk).
- Allows passing the owner to the scope block.
class Supplier < ApplicationRecord
has_one :account, ->(supplier) { where active: supplier.active? }
end
- Warning: Prevents preloading the association.
-
The
:counter_cache
option optimizes counting associated objects by storing count values in a separate column. -
Without a counter cache, author.books.size triggers a COUNT(*) query.
Implementation
- 1. Enable Counter Cache:
class Book < ApplicationRecord
belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord
has_many :books
end
- 2. Add Counter Cache Column:
class AddBooksCountToAuthors < ActiveRecord::Migration[8.0]
def change
add_column :authors, :books_count, :integer, default: 0, null: false
end
end
- 3. Custom Column Name:
class Book < ApplicationRecord
belongs_to :author, counter_cache: :count_of_books
end
Handling Large Tables
- Use
counter_cache: { active: false }
while backfilling values to avoid incorrect counts.
belongs_to :author, counter_cache: { active: false, column: :custom_books_count }
Resetting Counter Cache
- If data becomes stale (e.g., owner model’s primary key changes), use:
Author.reset_counters(author_id, :books)
-
Association callbacks allow executing methods at key points in a collection’s lifecycle.
-
Available callbacks:
before_add
after_add
before_remove
after_remove
class Author < ApplicationRecord
has_many :books, before_add: :check_credit_limit
def check_credit_limit(book)
throw(:abort) if limit_reached?
end
end
-
check_credit_limit
runs before a book is added to an author's collection. -
If
limit_reached?
returnstrue,
the book is not added.
-
Allows adding custom finders, creators, or other methods to an association.
-
Two ways to extend associations:
-
Inline in the model
-
Using a named module
-
Inline Extension
class Author < ApplicationRecord
has_many :books do
def find_by_book_prefix(book_number)
find_by(category_id: book_number[0..2])
end
end
end
Named Extension Module
module FindRecentExtension
def find_recent
where("created_at > ?", 5.days.ago)
end
end
class Author < ApplicationRecord
has_many :books, -> { extending FindRecentExtension }
end
class Supplier < ApplicationRecord
has_many :deliveries, -> { extending FindRecentExtension }
end
Advanced Extensions with proxy_association
module AdvancedExtension
def find_and_log(query)
results = where(query)
proxy_association.owner.logger.info("Querying #{proxy_association.reflection.name} with #{query}")
results
end
end
class Author < ApplicationRecord
has_many :books, -> { extending AdvancedExtension }
end
-
proxy_association
attributes:.owner
→ Returns the model instance the association belongs to..reflection
→ Returns the association’s metadata..target
→ Returns the associated objects.