Ecto

Ecto is an ORM, or more precisely a "language integrated query" (LINQ) library for Elixir.

Schema

defmodule App1.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer

    timestamps() # Adds :inserted_at and :updated_at fields
  end
end

Changeset

Change set is a way to track and validate changes to data before persisting them.

  • cast selects the allowed fields from input.
  • validate_* functions add errors if rules are broken.
  • changeset function is usually the place to define these rules, you may use separated functions for insert and update or other purpose.
defmodule App1.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer

    timestamps() # Adds :inserted_at and :updated_at fields
  end

  def changeset(user, attrs) do
    user
    |> Ecto.Changeset.cast(attrs, [:name, :email, :age])
    |> Ecto.Changeset.validate_required([:name, :email])
    |> Ecto.Changeset.validate_number(:age, greater_than_or_equal_to: 0)
    |> Ecto.Changeset.unique_constraint(:email, name: :users_email_index)
  end
end

Create a Changeset:

alias App1.User

%User{}
|> User.changeset(%{name: "Alice"})

#Ecto.Changeset<
  action: nil,
  changes: %{name: "Alice"},
  errors: [email: {"can't be blank", [validation: :required]}],
  data: #App1.User<>,
  valid?: false,
  ...
>

changeset.valid? is false and changeset.errors contains the validation messages. Typically these errors will returned to the user (e.g., in a web form).

Fix the Changeset:

%User{}
|> User.changeset(%{name: "Alice", email: "alice@example.com", age: 30})

#Ecto.Changeset<
  action: nil,
  changes: %{name: "Alice", email: "alice@example.com", age: 30},
  errors: [],
  data: #App1.User<>,
  valid?: true,
  ...
>

Now changeset.valid? is true and changeset.errors is empty. And this changeset can be inserted into the database using Repo.insert(changeset).

Repo

A repository module is needed to interact with the database.

defmodule App1.Repo do
  use Ecto.Repo,
    otp_app: :app1,
    adapter: Ecto.Adapters.Postgres
end

In config/config.exs:

config :app1, App1.Repo,
  url: System.get_env("DATABASE_URL"),

Inserting

alias App1.Repo

%User{}
|> User.changeset(%{name: "Alice", email: "alice@example.com", age: 30})
|> Repo.insert() # Returns {:ok, user} or {:error, changeset}

Updating

{:ok, user} = Repo.get(User, 1)
user
|> User.changeset(%{age: 7})
|> Repo.update()
User
  |> Query.where(id: 1)
  |> Repo.update_all(inc: [age: 1])

Query

Repo.all(App1.User)
Repo.get_by(App1.User, email: "alice@example.com")

Use the from macro:

import Ecto.Query

from u in User,
  where: u.age > 18,
  order_by: u.name,
  select: [u.id, u.email]
|> Repo.all

Since from is a macro, you need to unquote variables using ^:

from u in User,
  where: u.age > ^min_age

Tackle the N+1 Problem

users = Repo.preload(Repo.all(User, :posts))