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
end47.2 Migrations
mix ecto.gen.migration create_posts
mix ecto.migrate / ecto.rollback / ecto.reset / ecto.setupdefmodule 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
end47.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
end47.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) # upsert47.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.transaction47.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.