Active Record Basics

Ruby on Rails Fundamentals
Course 1 ยท Chapter 5 ยท Active Record Basics

๐Ÿ—„๏ธ Active Record Basics

Every chapter since Chapter 3 has used Post.find(params[:id]) or similar without explaining it. This chapter finally does: Active Record is Rails' built-in ORM (Object-Relational Mapper), and it replaces essentially everything Express's mysql2 integration required you to write by hand.

๐Ÿ—๏ธ Migrations โ€” Versioned Schema Changes

$ rails generate migration CreatePosts title:string body:text published:boolean
# db/migrate/20260101000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.1]
  def change
    create_table :posts do |t|
      t.string  :title
      t.text    :body
      t.boolean :published, default: false
      t.timestamps   # adds created_at and updated_at automatically
    end
  end
end

$ rails db:migrate

A migration is a single, ordered, timestamped Ruby file describing one schema change โ€” running rails db:migrate applies every migration that hasn't run yet, in order. This is the direct Rails equivalent of manually writing and tracking CREATE TABLE/ALTER TABLE SQL yourself against a mysql2 connection, with the ordering and "has this already run?" bookkeeping handled automatically instead of by convention you maintain yourself.

๐Ÿ“‹ schema.rb โ€” The Combined Snapshot

# db/schema.rb (auto-generated, never edited by hand)
ActiveRecord::Schema[7.1].define(version: 2026_01_01_000000) do
  create_table "posts" do |t|
    t.string  "title"
    t.text    "body"
    t.boolean "published", default: false
    t.datetime "created_at"
    t.datetime "updated_at"
  end
end

schema.rb is automatically regenerated after every migration โ€” a single, current source of truth for the entire database structure. Express has no direct equivalent at all; without a separate schema-tracking tool, the actual structure of a mysql2-backed database lives only in the database itself, discoverable only by connecting and inspecting it directly.

๐ŸŽญ Models โ€” Almost Suspiciously Empty

# app/models/post.rb
class Post < ApplicationRecord
end
โš  Yes, that's the entire model โ€” no column definitions anywhere

This is one of Active Record's biggest "magic" moments: Post automatically knows it has title, body, published, created_at, and updated_at attributes โ€” Rails introspects the actual database schema at runtime and generates matching accessor methods automatically. There's no column list to keep in sync inside the model file itself; the migration (and by extension, schema.rb) is the single source of truth. Compare this to mysql2, where every column has to be manually pulled out of a raw result row, field by field, every time.

๐Ÿ”ง Basic CRUD โ€” Active Record vs Raw SQL

The Same Five Operations, Both Ways

Operationmysql2 (Express)Active Record (Rails)
Get all rowsconnection.query("SELECT * FROM posts")Post.all
Find by IDconnection.query("SELECT * FROM posts WHERE id = ?", [id])Post.find(id)
Insertconnection.query("INSERT INTO posts (title) VALUES (?)", [title])Post.create(title: title)
Updateconnection.query("UPDATE posts SET title = ? WHERE id = ?", [title, id])post.update(title: title)
Deleteconnection.query("DELETE FROM posts WHERE id = ?", [id])post.destroy

โ›“๏ธ Chained Query Methods

Post.where(published: true)
    .order(created_at: :desc)
    .limit(5)

Each method returns another chainable query object rather than immediately hitting the database โ€” the actual SQL query is only built and executed once the result is enumerated (printed, looped over, etc.). This reads close to a plain-English sentence โ€” "posts that are published, ordered by newest, limited to five" โ€” and generates a single, efficient SQL query behind the scenes, comparable to method chaining Enumerable chains from both Ruby courses, just building SQL instead of transforming an in-memory array.

๐Ÿ›ก๏ธ Automatic SQL Injection Protection

# SAFE -- parameterized automatically
Post.where(title: params[:title])

# SAFE -- ? is a placeholder, Active Record escapes the value
Post.where("title = ?", params[:title])

# DANGEROUS -- raw string interpolation, exactly the vulnerability from the SQLi course
Post.where("title = '#{params[:title]}'")

Every one of Active Record's normal query methods โ€” where with a hash, find, create โ€” parameterizes values automatically, the same protection the completed SQL Injection course covered as essential. The dangerous pattern above is the exact same mistake covered there: building a query with raw string interpolation instead of a placeholder, whether in mysql2 or, as shown, even inside Active Record's own string-based where form if used carelessly.

๐Ÿ“œ Data Access, Side by Side

mysql2 vs Active Record

Concernmysql2 (Express)Active Record (Rails)
Schema changesHand-written SQL, tracked manuallyMigrations, tracked and versioned automatically
Current schema referenceNo built-in equivalentdb/schema.rb, auto-generated
Model definitionManually map every column yourselfEmpty class โ€” columns introspected at runtime
Query buildingRaw SQL strings with placeholdersChainable Ruby methods (where/order/limit)
Injection safetyManual โ€” must use placeholders correctly every timeAutomatic through the standard query interface

๐Ÿ’ป Coding Challenges

Challenge 1: A Comment Migration

Write the rails generate migration command (with column types) and the resulting migration file for a Comment model with post_id (integer), body (text), and author_name (string). Then write out what db/schema.rb would show for the comments table after running rails db:migrate.

Goal: Practice the full migration โ†’ schema.rb pipeline for a new table.

โ†’ Solution

Challenge 2: Raw SQL to Active Record

Convert this raw SQL into an equivalent chained Active Record query: SELECT * FROM posts WHERE published = 1 ORDER BY title ASC LIMIT 10.

Goal: Practice translating a query you already know how to write in SQL into Active Record's chainable method style.

โ†’ Solution

Challenge 3: Spot the SQL Injection Risk

Given two versions of a search action โ€” one using Post.where("title LIKE ?", "%#{params[:q]}%") and one using Post.where("title LIKE '%#{params[:q]}%'") โ€” identify which one is vulnerable to SQL injection and explain, in a sentence referencing the completed SQLi course, exactly why.

Goal: Be able to spot this specific vulnerability pattern even inside Active Record's own query interface, not just in raw mysql2 code.

โ†’ Solution

๐Ÿ’Ž Active Record Doesn't Remove SQL โ€” It Automates the Safe Version

It's worth being precise about what actually changed here: Active Record isn't hiding SQL entirely, it's generating well-formed, parameterized SQL for you based on the same relational concepts (tables, columns, WHERE clauses) already familiar from raw SQL or mysql2. The genuine win isn't "you never think about SQL again" โ€” it's that the default, easiest-to-write path is also the secure one, the same "secure by default" theme Chapter 4's automatic HTML escaping already introduced.

๐ŸŽฏ What's Next

Next chapter: Active Record Associations โ€” has_many, belongs_to, and how Rails handles relationships between tables that would otherwise require manually writing JOIN queries.