Active Record Basics
🗄️ Active Record Basics
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
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
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
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
| Operation | mysql2 (Express) | Active Record (Rails) |
|---|---|---|
| Get all rows | connection.query("SELECT * FROM posts") | Post.all |
| Find by ID | connection.query("SELECT * FROM posts WHERE id = ?", [id]) | Post.find(id) |
| Insert | connection.query("INSERT INTO posts (title) VALUES (?)", [title]) | Post.create(title: title) |
| Update | connection.query("UPDATE posts SET title = ? WHERE id = ?", [title, id]) | post.update(title: title) |
| Delete | connection.query("DELETE FROM posts WHERE id = ?", [id]) | post.destroy |
⛓️ Chained Query Methods
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
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
| Concern | mysql2 (Express) | Active Record (Rails) |
|---|---|---|
| Schema changes | Hand-written SQL, tracked manually | Migrations, tracked and versioned automatically |
| Current schema reference | No built-in equivalent | db/schema.rb, auto-generated |
| Model definition | Manually map every column yourself | Empty class — columns introspected at runtime |
| Query building | Raw SQL strings with placeholders | Chainable Ruby methods (where/order/limit) |
| Injection safety | Manual — must use placeholders correctly every time | Automatic 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.
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.
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.
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.