Lesson 17 / 25
Ecto: Schemas, Changesets and Queries
Persist data with Ecto schemas, validate with changesets and query safely.
Data access, explicitly
Ecto is Elixir's database toolkit, not a traditional ORM. Its parts: the Repo (Shop.Repo) performs all database operations (Repo.insert, Repo.get, Repo.all, Repo.transaction); schemas map database tables to structs (schema "products" do field :name, :string ... end) with associations such as belongs_to and has_many; changesets filter, cast and validate external data before it touches the database (cast/3, validate_required, validate_number, unique_constraint), collecting errors to show to users; migrations (mix ecto.gen.migration add_products) evolve the schema with create table and indexes; and the query DSL builds composable, parameterised SQL (from p in Product, where: p.price < ^max), safe from SQL injection because values are pinned as parameters. Associations are never loaded implicitly: use Repo.preload or preload: in queries, which avoids hidden N+1 queries. Ecto.Multi groups several operations into a single transaction. PostgreSQL is the most common database, with adapters for MySQL, SQLite and others.
Schema, changeset, migration and queries
Validation happens in the changeset; queries pin values as parameters.
# priv/repo/migrations/20261003120000_create_products.exs
defmodule Shop.Repo.Migrations.CreateProducts do
use Ecto.Migration
def change do
create table(:products) do
add :sku, :string, null: false
add :name, :string, null: false
add :price_paise, :integer, null: false
timestamps()
end
create unique_index(:products, [:sku])
end
end
# lib/shop/catalog/product.ex
defmodule Shop.Catalog.Product do
use Ecto.Schema
import Ecto.Changeset
schema "products" do
field :sku, :string
field :name, :string
field :price_paise, :integer
timestamps()
end
def changeset(product, attrs) do
product
|> cast(attrs, [:sku, :name, :price_paise])
|> validate_required([:sku, :name, :price_paise])
|> validate_number(:price_paise, greater_than: 0)
|> unique_constraint(:sku)
end
end
# lib/shop/catalog.ex (context)
defmodule Shop.Catalog do
import Ecto.Query
alias Shop.Repo
alias Shop.Catalog.Product
def create_product(attrs), do: %Product{} |> Product.changeset(attrs) |> Repo.insert()
def cheaper_than(max_paise) do
from(p in Product, where: p.price_paise < ^max_paise, order_by: [asc: p.price_paise])
|> Repo.all()
end
end
# Shop.Catalog.create_product(%{"sku" => "pen", "name" => "Pen", "price_paise" => 0})
# => {:error, %Ecto.Changeset{errors: [price_paise: {"must be greater than %{number}", ...}]}}A security check at the door
A changeset is the security check at the entrance: it inspects every bag (field), lets through only what is allowed, and writes down everything wrong, so problems are reported politely instead of causing chaos inside the database.
Quick check: What is the main job of an Ecto changeset?
- To open database connections
- To cast and validate external data and track changes and errors before persisting
- To render HTML
- To start processes
Answer
To cast and validate external data and track changes and errors before persisting — Changesets filter, cast and validate data, collecting errors.