---
title: "Phoenix: Ecto"
category: Elixir
layout: 2017/sheet
tags: [WIP]
updated: 2017-08-30
---

## Schemas
{: .-three-column}

### Generating

```bash
$ mix phx.gen.html \
    Accounts \       # domain
    Profile \        # schema
    profiles \       # table name
    email:string \
    age:integer
```

### Schema

```elixir
defmodule Myapp.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :name
    field :age, :integer
    field :password, virtual: true

    timestamps()
  end
end
```

### Field types

| Field |
| --- |
| `:id` |
| `:binary` |
| `:boolean` |
| `:string` |
| --- |
| `:integer` |
| `:float` |
| `:decimal` |
| --- |
| `{:array, inner_type}` |
| `:map` |
{: .-left-align}

## Changesets

### Changesets

```elixir
def changeset(user, params \\ :empty) do
  %User{}
  |> Ecto.Changeset.change   # basic casting to changeset

  user
  |> cast(params, ~w(name email), ~w(age)) # params to Changeset

  |> validate_format(:email, ~r/@/)

  |> validate_inclusion(:age, 18..100)
  |> validate_exclusion(:role, ~w(admin superadmin))
  |> validate_subset(:pets, ~w(cat dog parrot whale))

  |> validate_length(:body, min: 1)
  |> validate_length(:body, min: 1, max: 160)
  |> validate_length(:partners, is: 2)

  |> validate_number(:pi, greater_than: 3)
  |> validate_number(:pi, less_than: 4)
  |> validate_number(:pi, equal_to: 42)

  |> validate_change(:title, fn _, _ -> [])
  |> validate_confirmation(:password, message: "does not match")

  |> unique_constraint(:email)
  |> foreign_key_constraint(:post_id)
  |> assoc_constraint(:post)      # ensure post_id exists
  |> no_assoc_constraint(:post)   # negative (useful for deletions)
end
```

### Changeset fields

```elixir
changeset.valid?
changeset.errors     #=> [title: "empty"]

changeset.changes    #=> %{}
changeset.params[:title]

changeset.required   #=> [:title]
changeset.optional   #=> [:body]
```

### Updating

```elixir
changeset #(or model)
|> change(title: "New title")
|> change(%{ title: "New title" })
|> put_change(:title, "New title")
|> force_change(:title, "New title")
|> update_change(:title, &(&1 <> "..."))

|> delete_change(:title)
|> merge(other_changeset)

|> add_error(:title, "empty")
```

### Getting

```elixir
get_change(changeset, :title)    #=> "hi" (if changed)
get_field(changeset, :title)     #=> "hi" (even if unchanged)

fetch_change(changeset, :title)  #=> {:ok, "hi"} | :error
fetch_field(changeset, :title)   #=> {:changes | :model, "value"} | :error
```

## Repo

### Get one

```elixir
Repo.get(User, id)
Repo.get_by(User, email: "john@hello.com")  #=> %User{} | nil

# also get! get_by!
```

### Create/update

```elixir
changeset |> Repo.update
changeset |> Repo.insert
changeset |> Repo.insert_or_update
```

```
User
|> Ecto.Changeset.change(%{name: "hi"})
|> Repo.insert
```

## Many

### Queries

```elixir
from p in Post,
  where: p.title == "Hello",
  where: [state: "Sweden"],

  limit: 1,
  offset: 10,

  order_by: c.name,
  order_by: [c.name, c.title],
  order_by: [asc: c.name, desc: c.title],

  preload: [:comments],
  preload: [comments: {c, likes: l}],

  join: c in assoc(c, :comments),
  join: p in Post, on: c.post_id == p.id,
  group_by: p,

  select: p,
  select: {p.title, p.description},
  select: [p.title, p.description],
```

### Get many

```elixir
Repo.all(User)
```

### Update many

```elixir
Repo.update_all(Post, set: [title: "Title"])
Repo.update_all(Post, inc: [views: 1])
```

### Chaining `_all` with queries

```elixir
from(p in Post, where: p.id < 10)
|> Repo.update_all(...)

from(p in Post, where: p.id < 10)
|> Repo.all()
```

## References
{: .-one-column}

- Based on Ecto 1.3.
