After understanding routes, I was ready to build something real. But I quickly hit a wall: where do I store data? How do I save user information? How does Rails talk to the database?
This is where models come in. In this post, I'll walk you through creating your first Rails model by building a simple task manager app—the same project that finally made ActiveRecord click for me.
What Are Models in Rails?
Models are Ruby classes that represent data in your database. They handle:
- Database interactions (saving, reading, updating, deleting)
- Data validation (ensuring email formats are correct, required fields are present)
- Business logic (calculating totals, processing data)
- Relationships between different types of data
Think of models as the "smart" part of your data—they know how to save themselves, validate themselves, and relate to other data.
The Task Manager: Our First Real App
Let's build a simple task manager where users can:
- Create tasks with titles and descriptions
- Mark tasks as complete or incomplete
- Set due dates
- Add priority levels
This will teach us the fundamentals of Rails models without getting too complex.
Creating Our First Model
Rails provides a generator to create models:
rails generate model Task title:string description:text completed:boolean due_date:date priority:integer
This command creates several files:
- app/models/task.rb
- The model class
- db/migrate/xxx_create_tasks.rb
- Database migration
- test/models/task_test.rb
- Test file
Let's examine what each does.
Understanding Migrations
The migration file defines how to create the database table:
# db/migrate/20231201000000_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[7.0]
def change
create_table :tasks do |t|
t.string :title
t.text :description
t.boolean :completed
t.date :due_date
t.integer :priority
t.timestamps
end
end
end
Key points:
- t.timestamps
automatically adds created_at
and updated_at
columns
- Each data type (string
, text
, boolean
, etc.) maps to database column types
- Migrations are like version control for your database
Run the migration to create the table:
bash
rails db:migrate
The Model Class
Here's our basic model:
# app/models/task.rb
class Task < ApplicationRecord
# This is all Rails needs to connect to the database!
end
That's it! By inheriting from ApplicationRecord
, our Task model automatically gets methods to:
- Create tasks: Task.create(title: "Learn Rails")
- Find tasks: Task.find(1)
or Task.all
- Update tasks: task.update(completed: true)
- Delete tasks: task.destroy
Testing Our Model in the Console
Let's play with our model in the Rails console:
rails console
# Create a new task
task = Task.new(title: "Learn Rails Models", description: "Build a task manager")
task.save
# Or create and save in one step
task = Task.create(
title: "Write blog post",
description: "Explain Rails models to beginners",
priority: 1,
due_date: Date.tomorrow
)
# Find tasks
Task.all # Returns all tasks
Task.first # Returns first task
Task.find(1) # Find task with ID 1
Task.find_by(title: "Learn Rails Models") # Find by any attribute
# Update a task
task = Task.first
task.update(completed: true)
# Delete a task
task.destroy
Adding Validations
Raw database storage isn't enough—we need to ensure data quality. Let's add validations:
# app/models/task.rb
class Task < ApplicationRecord
validates :title, presence: true, length: { minimum: 3, maximum: 100 }
validates :priority, inclusion: { in: 1..5, message: "must be between 1 and 5" }
validates :due_date, presence: true
# Custom validation
validate :due_date_cannot_be_in_the_past
private
def due_date_cannot_be_in_the_past
if due_date.present? && due_date < Date.current
errors.add(:due_date, "can't be in the past")
end
end
end
Now test these validations:
# In rails console
task = Task.new(title: "")
task.valid? # Returns false
task.errors.full_messages # Shows validation errors
task = Task.new(title: "Test", due_date: Date.yesterday)
task.save # Returns false because due_date is in the past
task.errors[:due_date] # Shows specific error for due_date field
Adding Useful Methods
Models can have custom methods for business logic:
# app/models/task.rb
class Task < ApplicationRecord
validates :title, presence: true, length: { minimum: 3, maximum: 100 }
validates :priority, inclusion: { in: 1..5 }
validates :due_date, presence: true
validate :due_date_cannot_be_in_the_past
# Scopes for common queries
scope :completed, -> { where(completed: true) }
scope :pending, -> { where(completed: false) }
scope :high_priority, -> { where(priority: [1, 2]) }
scope :due_soon, -> { where(due_date: Date.current..3.days.from_now) }
# Instance methods
def overdue?
due_date < Date.current && !completed?
end
def priority_name
case priority
when 1 then "Critical"
when 2 then "High"
when 3 then "Medium"
when 4 then "Low"
when 5 then "Someday"
else "Unknown"
end
end
def complete!
update(completed: true)
end
def status
if completed?
"Completed"
elsif overdue?
"Overdue"
else
"Pending"
end
end
private
def due_date_cannot_be_in_the_past
if due_date.present? && due_date < Date.current
errors.add(:due_date, "can't be in the past")
end
end
end
Using Scopes and Methods
Scopes provide convenient ways to query data:
# In rails console
Task.completed # All completed tasks
Task.pending.high_priority # High priority pending tasks
Task.due_soon # Tasks due in the next 3 days
# Instance methods
task = Task.first
task.priority_name # "High" instead of just "2"
task.overdue? # true/false
task.complete! # Marks task as completed
Database Indexes for Performance
As your app grows, you'll want to add indexes for frequently queried columns:
rails generate migration AddIndexesToTasks
# db/migrate/xxx_add_indexes_to_tasks.rb
class AddIndexesToTasks < ActiveRecord::Migration[7.0]
def change
add_index :tasks, :completed
add_index :tasks, :due_date
add_index :tasks, :priority
add_index :tasks, [:completed, :due_date] # Composite index
end
end
Default Values
Set default values for certain fields:
# In the migration
class CreateTasks < ActiveRecord::Migration[7.0]
def change
create_table :tasks do |t|
t.string :title
t.text :description
t.boolean :completed, default: false # Default to incomplete
t.date :due_date
t.integer :priority, default: 3 # Default to medium priority
t.timestamps
end
end
end
Or in the model:
# app/models/task.rb
class Task < ApplicationRecord
after_initialize :set_defaults
private
def set_defaults
self.completed ||= false
self.priority ||= 3
end
end
Common Model Mistakes I Made
1. Forgetting Validations
# Bad - no validation
class Task < ApplicationRecord
end
# Good - validates important fields
class Task < ApplicationRecord
validates :title, presence: true
validates :due_date, presence: true
end
2. Putting View Logic in Models
# Bad - HTML in model
def formatted_title
"<h2>#{title}</h2>".html_safe
end
# Good - data formatting only
def title_with_priority
"[#{priority_name}] #{title}"
end
3. Fat Models
# Bad - model doing too much
class Task < ApplicationRecord
def send_reminder_email
# 20 lines of email logic
end
def calculate_team_productivity
# 30 lines of calculation logic
end
end
# Good - extract to service objects
class Task < ApplicationRecord
def send_reminder
TaskReminderService.new(self).send_email
end
end
Testing Your Model
Always test your model logic:
# test/models/task_test.rb
require "test_helper"
class TaskTest < ActiveSupport::TestCase
test "should not save task without title" do
task = Task.new(due_date: Date.tomorrow)
assert_not task.save
end
test "should not save task with past due date" do
task = Task.new(title: "Test", due_date: Date.yesterday)
assert_not task.save
assert_includes task.errors[:due_date], "can't be in the past"
end
test "completed scope returns only completed tasks" do
completed_task = Task.create(title: "Done", due_date: Date.tomorrow, completed: true)
pending_task = Task.create(title: "Todo", due_date: Date.tomorrow, completed: false)
assert_includes Task.completed, completed_task
assert_not_includes Task.completed, pending_task
end
test "overdue? returns true for past due incomplete tasks" do
task = Task.create(title: "Overdue", due_date: Date.yesterday, completed: false)
assert task.overdue?
end
end
Run tests with:
bash
rails test test/models/task_test.rb
What I Learned Building This
- Models are more than database wrappers - They contain business logic and data rules
- Validations save you from bad data - Always validate user input
- Scopes make queries readable -
Task.pending.high_priority
is clearer thanTask.where(completed: false, priority: [1,2])
- The Rails console is invaluable - Test your models interactively before building views
- Start simple, then add complexity - Begin with basic CRUD, then add validations and methods
Next Steps
Now that you have a working model, you need ways for users to interact with it. That means building controllers and views to create, display, and manage tasks.
In the next post, we'll create controllers that use our Task model and build forms where users can create and edit tasks.
Key takeaways:
- Models represent data and business logic
- Migrations create and modify database tables
- Validations ensure data quality
- Scopes provide convenient query methods
- Custom methods add business logic
- Test your models to ensure they work correctly
Try building your own model! Start with something simple like a Book model with title, author, and publication year. The patterns are the same regardless of what data you're modeling.
Next up: "Adding User Authentication Without Gems" - We'll build login/logout functionality from scratch to understand how authentication really works.