🧱 Fase 3: Persistência — A Camada de Dados (Ecto, Repo, Migration e Schema)¶
Na fase anterior, nossa aplicação mantinha as tarefas apenas em memória. Agora daremos um passo importante: armazenar os dados de forma permanente usando o Ecto, a biblioteca de persistência do Elixir.
Optamos por não incluir o Ecto desde o início (--no-ecto) para que você entendesse primeiro a lógica interna do LiveView. Agora veremos como adicionar novas dependências a um projeto existente e configurar o banco de dados real.
Nesta fase, montamos apenas a camada de dados (dependências, Repo, migration e schema) e a testamos isoladamente. O "transplante" — trocar o coração em memória do TodoLive pelo banco — fica para a Fase 4.
⚙️ Passo 3.1: Adicionar as Dependências do Ecto¶
Pare o servidor (Ctrl+C duas vezes) e abra o arquivo mix.exs, o equivalente ao package.json (JavaScript) ou requirements.txt (Python). É nele que declaramos as dependências do projeto, dentro da função defp deps do.
Warning
NÃO substitua a lista de dependências! O phx.new já gerou uma lista grande (phoenix, phoenix_html, live_view, tailwind, esbuild, bandit e mais uma dúzia) — todas são necessárias. Nossa tarefa é apenas acrescentar quatro linhas a essa lista. Substituir a lista inteira por uma menor quebraria o projeto por completo.
Ação: localize defp deps do no mix.exs e adicione estas quatro linhas em qualquer ponto da lista (por exemplo, logo após a linha do {:phoenix_live_view, ...}), mantendo tudo o que já existe:
defp deps do
[
{:phoenix, "~> 1.8.1"},
# ... (todas as dependências que o phx.new gerou — NÃO apague nada!) ...
# --- ADICIONE ESTAS 4 LINHAS (suporte ao Ecto) ---
{:ecto, "~> 3.11"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.10"},
{:ecto_sqlite3, "~> 0.12"}, # SQLite: banco leve, baseado em arquivo
# --------------------------------------------------
# ... (o restante das dependências geradas) ...
]
end
O que cada uma faz:
| Dependência | Papel |
|---|---|
ecto |
O núcleo: schemas, changesets e queries. |
ecto_sql |
A camada SQL do Ecto (migrations, adaptadores). |
ecto_sqlite3 |
O "driver" específico do SQLite. |
phoenix_ecto |
A cola entre o Phoenix e o Ecto (ex: integração de changesets com formulários). |
Em seguida, baixe as dependências:
🧩 Passo 3.2: Configurar o "Repo" — o Agente do Banco de Dados¶
O Repo (de repository) é o módulo que representa a conexão com o banco. É como o settings.py do Django ou o database.yml do Rails: ele sabe onde está o banco e como acessá-lo.
Ação 1: crie o arquivo lib/elixir_todo_list/repo.ex:
defmodule ElixirTodoList.Repo do
use Ecto.Repo,
otp_app: :elixir_todo_list,
adapter: Ecto.Adapters.SQLite3
end
Ação 2: agora precisamos supervisionar o Repo — garantir que ele seja iniciado (e reiniciado, se cair) junto com a aplicação. Abra lib/elixir_todo_list/application.ex e adicione o Repo no início da lista de processos supervisionados:
children = [
ElixirTodoList.Repo, # 👈 ADICIONE ESTA LINHA (antes dos demais)
ElixirTodoListWeb.Telemetry,
{DNSCluster, query: Application.get_env(:elixir_todo_list, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: ElixirTodoList.PubSub},
# ...
ElixirTodoListWeb.Endpoint
]
Tip
Essa lista children é a árvore de supervisão — um dos superpoderes do Elixir. Cada item é um processo que a aplicação inicia e vigia. Se o Repo travar, o supervisor o reinicia automaticamente. É o mesmo mecanismo que "ressuscitou" nosso LiveView na Fase 2!
Ação 3: por fim, configure o Repo no arquivo config/config.exs. Adicione (por exemplo, logo após a linha import Config no topo):
config :elixir_todo_list, ElixirTodoList.Repo,
database: "elixir_todo_list.db",
priv: "priv/repo"
config :elixir_todo_list, ecto_repos: [ElixirTodoList.Repo]
Entendendo as opções:
database:→ o arquivo onde o SQLite salvará tudo. Com esse valor, ele será criado na raiz do projeto (elixir_todo_list.db).priv:→ onde ficam os arquivos de apoio do Repo — em especial, as migrations (priv/repo/migrations/). Atenção: isso não muda o local do banco!ecto_repos:→ informa às tarefas do Mix (mix ecto.create,mix ecto.migrate) quais Repos existem.
Tip
Lembra do .gitignore da Fase 0? As regras *.db, *.db-shm e *.db-wal foram escritas exatamente para este momento: o banco (e seus arquivos auxiliares de escrita) vão aparecer na raiz do projeto e não devem ser versionados.
🏗️ Passo 3.3: Criar o Banco de Dados¶
O banco ainda não existe. Crie-o com:
Resultado Esperado:
Olhe a raiz do projeto: o arquivo elixir_todo_list.db apareceu. (E rode git status para confirmar que ele não aparece para o Git — obrigado, .gitignore!)
🧬 Passo 3.4: Migrations — a "Planta Baixa" do Banco¶
Em praticamente todos os frameworks modernos (Django, Rails, Laravel), usamos migrations para versionar e aplicar mudanças no banco. Cada migration é um pequeno "passo evolutivo": criar uma tabela, adicionar uma coluna, etc.
Ação 1: gere a migration da nossa tabela de tarefas:
Isso cria um arquivo dentro de priv/repo/migrations/ (o nome começa com um timestamp — é assim que o Ecto sabe a ordem de aplicação).
Ação 2: abra o arquivo gerado e complete a função change:
defmodule ElixirTodoList.Repo.Migrations.CreateTasksTable do
use Ecto.Migration
def change do
create table(:tasks) do
add :title, :string
add :completed, :boolean, default: false
timestamps() # Adiciona as colunas inserted_at e updated_at
end
end
end
Ação 3: aplique a migration:
Saída esperada:
[info] == Running ... ElixirTodoList.Repo.Migrations.CreateTasksTable.change/0 forward
[info] create table tasks
[info] == Migrated ... in 0.0s
📘 Passo 3.5: Conceitos-Chave — Schema, Changeset e Form¶
Para conectar o código à tabela recém-criada, precisamos de três peças:
| Conceito | O que é | Analogia |
|---|---|---|
| Schema | Define a estrutura da tabela e cria uma struct %Task{} correspondente. |
O "model" do Django ou o ActiveRecord do Rails. |
| Changeset | Um conjunto de regras para validar e transformar dados antes de salvar. | O ModelForm do Django. |
to_form/1 |
Converte o changeset para o formato que o LiveView usa no <.form>. |
Um form object no MVC. |
🧩 Passo 3.6: Criar o Schema Task¶
Crie o arquivo lib/elixir_todo_list/task.ex:
defmodule ElixirTodoList.Task do
use Ecto.Schema
import Ecto.Changeset
# "schema" espelha a tabela "tasks" no banco
schema "tasks" do
field :title, :string
field :completed, :boolean, default: false
timestamps(type: :utc_datetime)
end
# Define como validar os dados antes de salvar
def changeset(task_struct, attrs) do
task_struct
|> cast(attrs, [:title, :completed])
|> validate_required([:title])
end
end
cast/3→ filtra os dados de entrada, aceitando apenas os campos listados (:title,:completed) — proteção contra dados indesejados;validate_required/2→ garante que:titlenão está vazio. É esta linha que fará o formulário exibir "can't be blank" na Fase 4.
🧪 Passo 3.7: Testando a Camada de Dados no IEx (sem o LiveView!)¶
Assim como fizemos no REPL do Clojure, vamos provar que a camada de dados funciona antes de conectá-la à interface. A versão Elixir do REPL é o IEx (Interactive Elixir), e podemos iniciá-lo com o projeto carregado:
No prompt iex(1)>, experimente:
# Atalhos para digitar menos
iex> alias ElixirTodoList.{Repo, Task}
# 1. O banco está vazio?
iex> Repo.all(Task)
[]
# 2. Crie uma tarefa (validando com o changeset)
iex> %Task{} |> Task.changeset(%{title: "Testar o IEx"}) |> Repo.insert()
{:ok, %ElixirTodoList.Task{id: 1, title: "Testar o IEx", completed: false, ...}}
# 3. E uma inválida? (sem título)
iex> %Task{} |> Task.changeset(%{}) |> Repo.insert()
{:error, #Ecto.Changeset<..., errors: [title: {"can't be blank", ...}], valid?: false>}
# 4. Confira a lista
iex> Repo.all(Task)
[%ElixirTodoList.Task{id: 1, title: "Testar o IEx", ...}]
Momento "Aha!": repare nas tuplas {:ok, ...} e {:error, changeset} — são exatamente os dois casos que trataremos com case no LiveView, na próxima fase. E a tarefa inválida não foi salva: o changeset barrou antes de chegar ao banco.
Saia do IEx com Ctrl+C duas vezes.
💾 Passo 3.8: Commit (Camada de Dados)¶
Fim da Fase 3! 🏁
Agora temos a estrutura completa:
- O banco de dados criado e versionado por migrations;
- O
Repogerenciando conexões e consultas (supervisionado!); - O schema
Taskrefletindo nossa tabela, com validação via changeset — tudo testado no IEx.
Falta o passo mais gratificante: trocar o "coração" em memória do TodoLive por esse novo, persistente.