Skip to content
AgileLogics
7 min read

Working with Databases

Prerequisites: Exceptions in depth, Gems and Bundler

Most real applications store data in a database. This lesson uses SQLite — a complete SQL database in a single file, with nothing to install beyond a gem — and the ideas carry over directly to PostgreSQL and MySQL.

bundle add sqlite3

Creating a table and inserting rows

require "sqlite3"

db = SQLite3::Database.new(":memory:")   # or a file: "app.db"
db.results_as_hash = true

db.execute <<~SQL
  CREATE TABLE users (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE
  )
SQL

db.execute("INSERT INTO users (name, email) VALUES (?, ?)", ["Ada", "ada@example.com"])
db.execute("INSERT INTO users (name, email) VALUES (?, ?)", ["Linus", "linus@example.com"])

rows = db.execute("SELECT id, name FROM users WHERE name LIKE ?", ["A%"])
rows.each { |row| puts "#{row["id"]}: #{row["name"]}" }

p db.get_first_value("SELECT COUNT(*) FROM users")
1: Ada
2

Always use placeholders

The ? marks are placeholders. The values are sent separately from the SQL, so the database never treats them as SQL code. Never build SQL by inserting values into the string:

# DANGEROUS — SQL injection
db.execute("SELECT * FROM users WHERE email = '#{params[:email]}'")

If someone submits ' OR '1'='1 as their email, that query returns every user. Placeholders make this whole class of attack impossible. The SQL Formatter is useful for reading long queries.

Transactions

A transaction groups several changes so they all succeed or all fail together — essential for anything like moving money between accounts:

db.transaction do
  db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", [50, 1])
  db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", [50, 2])
end

If an exception is raised inside the block, both updates are rolled back.

ORMs and query builders

Writing SQL by hand gets repetitive. Most Ruby projects use a library:

  • Active Record (part of Rails) — an ORM (object-relational mapper): each table is a class, each row an object — User.where(active: true).order(:name).
  • Sequel — a flexible toolkit usable with or without models, popular outside Rails.

Both use placeholders for you. Both also make N+1 queries easy to write by accident — loading 100 users, then running one extra query per user for their orders. Load related data up front (User.includes(:orders) in Active Record) instead.

Common mistakes

  • String interpolation in SQL. Always use placeholders or the ORM's query methods.
  • Running a query inside a loop over query results (N+1).
  • Doing multi-step changes without a transaction.