Ecto

Ecto is a toolkit for data mapping and querying (not a classic ORM): Repo, Schema, Changeset, Query, Migration.

Ecto is a toolkit for data mapping and querying (not a classic ORM): Repo, Schema, Changeset, Query, Migration.

47.1 Repo

Adapters: Postgres (postgrex), MySQL (myxql), SQLite (ecto_sqlite3), MSSQL (tds).

defmodule Blog.Repo do
  use Ecto.Repo, otp_app: :blog, adapter: Ecto.Adapters.Postgres
end

47.2 Migrations

mix ecto.gen.migration create_posts
mix ecto.migrate / ecto.rollback / ecto.reset / ecto.setup
defmodule Blog.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:posts) do
      add :title, :string, null: false
      add :body, :text
      add :views, :integer, default: 0
      add :author_id, references(:users, on_delete: :delete_all)
      timestamps(type: :utc_datetime)
    end

    create index(:posts, [:author_id])
    create unique_index(:posts, [:title])
  end
end

47.3 Schemas

Schemaless changesets ({data, types}) and embedded_schema validate forms without a table.

defmodule Blog.Posts.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string
    field :body, :string
    field :views, :integer, default: 0
    field :status, Ecto.Enum, values: [:draft, :published]
    field :word_count, :integer, virtual: true
    belongs_to :author, Blog.Accounts.User
    has_many :comments, Blog.Posts.Comment
    many_to_many :tags, Blog.Posts.Tag, join_through: "posts_tags"
    embeds_many :revisions, Revision   # stored as JSON
    timestamps(type: :utc_datetime)
  end

  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :body, :status, :author_id])
    |> validate_required([:title, :body])
    |> validate_length(:title, min: 3, max: 100)
    |> validate_inclusion(:status, [:draft, :published])
    |> unique_constraint(:title)
    |> foreign_key_constraint(:author_id)
  end
end

47.4 Changesets

cs = Post.changeset(%Post{}, %{"title" => "Hi"})
cs.valid?          # false
cs.errors          # [title: {"should be at least 3 character(s)", ...}, body: {"can't be blank", ...}]
cs.changes
get_change(cs, :title), put_change(cs, :views, 1), add_error(cs, :title, "bad")
validate_format(cs, :email, ~r/@/), validate_number(cs, :age, greater_than: 0)
validate_change(cs, :title, fn :title, t -> if t =~ "spam", do: [title: "no spam"], else: [] end)

47.5 CRUD

Repo.insert(Post.changeset(%Post{}, attrs))      # {:ok, post} | {:error, changeset}
Repo.get(Post, 1), Repo.get!(Post, 1), Repo.get_by(Post, title: "Hi")
Repo.all(Post), Repo.one(query), Repo.exists?(query), Repo.aggregate(Post, :count)
post |> Post.changeset(%{title: "New"}) |> Repo.update()
Repo.delete(post)
Repo.insert_all(Post, [%{title: "a", body: "b", inserted_at: now, updated_at: now}])
Repo.update_all(Post, inc: [views: 1])
Repo.insert(cs, on_conflict: :nothing, conflict_target: :title)   # upsert

47.6 Queries

import Ecto.Query

# Keyword syntax
query = from p in Post,
  where: p.views > 100 and p.status == :published,
  order_by: [desc: p.inserted_at],
  limit: 10,
  select: {p.id, p.title}

# Pipe (macro) syntax — composable
Post
|> where([p], p.author_id == ^author_id)       # ^ interpolates (safely parameterized)
|> join(:left, [p], c in assoc(p, :comments))
|> group_by([p], p.id)
|> select([p, c], %{post: p, comment_count: count(c.id)})
|> Repo.all()

# Preloading associations (avoid N+1)
Repo.all(from p in Post, preload: [:author, comments: :author])
Repo.preload(post, :comments)

# Fragments & dynamic queries
from p in Post, where: fragment("lower(?) LIKE ?", p.title, ^"%elixir%")
dynamic([p], p.views > ^min)
subquery(...), union(...), with_cte(...), windows/over(...)
Repo.stream(query) |> Stream.each(...) |> Stream.run()   # inside Repo.transaction

47.7 Transactions & Multi

Repo.transact/2 (Ecto 3.13+) is a simpler alternative returning {:ok, _} / {:error, _}.

Repo.transaction(fn ->
  {:ok, a} = Repo.update(debit)
  {:ok, b} = Repo.update(credit)
  {a, b}
end)

alias Ecto.Multi
Multi.new()
|> Multi.update(:debit, debit_changeset)
|> Multi.update(:credit, credit_changeset)
|> Multi.run(:audit, fn repo, %{debit: d} -> repo.insert(Audit.new(d)) end)
|> Repo.transaction()
# {:ok, %{debit: _, credit: _, audit: _}} | {:error, failed_step, value, changes_so_far}

47.8 Seeds & testing

priv/repo/seeds.exs → mix run priv/repo/seeds.exs. Test data with ExMachina factories.