Elixir: Data & Ecto

Ecto is not an ORM in the ActiveRecord sense — it is a functional data toolkit: explicit schemas, validations collected in changesets, composable queries, and a repository that owns the database connection. The flow is always: schema → changeset → repo.

The Ecto Flow

Data flows from user params through a schema, a changeset with validations, and the Repo into the database
Fig. 1 — Changesets are pipelines of validation: invalid input never reaches the Repo, and errors accumulate in a struct you can render.
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :role, Ecto.Enum, values: [:user, :admin], default: :user
    field :visits, :integer, default: 0
    timestamps()                       # inserted_at / updated_at
  end

  # A changeset is a pipeline: it either validates or accumulates errors.
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :role])    # whitelist input — nothing else lands
    |> validate_required([:email])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:email, max: 160)
    |> unique_constraint(:email)       # enforced by a DB index, not memory
  end
end
# Using the changeset — invalid input is a VALUE you inspect.
%MyApp.Accounts.User{}
|> MyApp.Accounts.User.changeset(%{"email" => "not-an-email"})
|> Repo.insert()

#=> {:error, %Ecto.Changeset{errors: [email: {"has invalid format", []}]}}
#   render `changeset.errors` straight to the API client.

Queries

Ecto.Query is a macro DSL that is compiled against your schema — typos in field names are compile errors, and there is no string concatenation to inject.

import Ecto.Query

# Keyword syntax — compile-checked against the schema.
query =
  from u in MyApp.Accounts.User,
    where: u.role == :user and u.visits > ^min_visits,   # ^ pins a value
    order_by: [desc: u.visits],
    limit: 10

Repo.all(query)

# Pipe syntax — composable pieces for dynamic conditions.
MyApp.Accounts.User
|> where([u], u.role == ^role)
|> select([u], {u.id, u.email})
|> Repo.all()

Migrations and Transactions

mix ecto.create                     # create the database
mix ecto.gen.migration add_visits_index
mix ecto.migrate                    # apply pending migrations
# A migration — forward AND down, reviewable in git.
defmodule MyApp.Repo.Migrations.AddVisitsIndex do
  use Ecto.Migration

  def change do
    create index(:users, [:visits])
    create unique_index(:users, [:email])   # backs unique_constraint/2
  end
end

# Transactions: Repo.transaction takes either a function or a Multi.
MyApp.Repo.transaction(fn ->
  {:ok, user} = MyApp.Repo.insert(changeset)
  {:ok, _} = create_profile(user)
  user
end)

# Multi composes dependent steps with named results — better than
# a function when the steps have complex error handling.
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, changeset)
|> Ecto.Multi.insert(:audit, fn %{user: user} -> audit_changeset(user) end)
|> MyApp.Repo.transaction()

Embedded Schemas

Not every shape needs a table. embedded_schema gives you struct + changeset machinery for form objects, JSON payloads, or values embedded in a JSON column.

defmodule MyApp.Search.Filters do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :query, :string
    field :min_price, :integer, default: 0
  end

  def changeset(filters \\ %__MODULE__{}, attrs) do
    filters
    |> cast(attrs, [:query, :min_price])
    |> validate_number(:min_price, greater_than_or_equal_to: 0)
  end
end

# Parse and validate request params without touching the database.
filters = MyApp.Search.Filters.changeset(%{"query" => "elixir", "min_price" => "-5"})
filters.valid?     #=> false — validation ran, no DB involved

Practice

  1. Model a Post with a belongs_to author, then write a query that preloads comments with a count.
  2. Demonstrate a unique_constraint firing on a duplicate email and render the error message.
  3. Replace one multi-step transaction function with an Ecto.Multi and compare error reporting.

Next: Phoenix Web Framework