Active Record Basics

Ruby on Rails Fundamentals — 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 Associationshas_many, belongs_to, and how Rails handles relationships between tables that would otherwise require manually writing JOIN queries.