Introdução
Sejam bem-vindos à introdução ao Ecto (um projeto onde realizei uma apresentação na Idopter Labs).
Esse projeto tem como objetivo auxiliar todos os desenvolvedores iniciantes na programação funcional.
Fico imensamente feliz com seu interesse em aprender um pouco sobre o Ecto. Espero conseguir fazer você entender todo o básico necessário para que você possa sair daqui já com conhecimento para desenvolver pequenas funcionalidades utilizando Ecto.
O que é o Ecto?
Ecto é basicamente uma lib para você interagir com seu banco. Então, podemos dizer que ele é uma camada que vai ficar entre seu banco de dados e sua aplicação.
Vale ressaltar, que o Ecto não é um "ORM" (Object Relation Mapping). Pois, ORM é sobre objetos e Elixir é uma linguagem funcional e não temos objetos em elixir.
Arquitetura do Ecto
A biblioteca Ecto é composta por quatro módulos principais:
-
Repo: Este módulo permite conexões com o banco. E é com ele que podemos criar/atualizar/deletar recursos e executar queries. Dito isso, é necessário um adapter específico para o SGBD (sistema de gerenciamento de banco de dados). Para conectarmos a banco de dados relacionais SQL usamos a biblioteca
Ecto.SQL
. - Schema: Mapeamento de dados entre código Elixir e estruturas SQL (tabelas, colunas, etc.)
-
Query: Módulo que permite fazer queries no banco de dados através de uma DSL em Elixir de uma forma mais fácil e segura, evitando
SQL Injection
. - Changeset: É um módulo que permite normalizar e validar dados da aplicação.
Operações Básicas
Cast e Validações
O Changeset recebe uma struct e com isso ele consegue tanto fazer cast de dados para inserir nessa struct como também fazer validações e modificações.
A função cast pega os parâmetros e tenta fazer o cast nos campos da struct e como segundo argumento temos que definir uma lista de campos para cast.
defmodule Ecto4noobs.User do
use Ecto.Schema
import Ecto.Changeset
@required_params [:age, :email, :name]
schema "users" do
...
end
def changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, @required_params)
end
end
Agora vamos no nosso iex criar um map:
iex> user_params = %{name: "Rômulo", email: "romulo@tomate.com", age: 23}
%{age: 23, email: "romulo@tomate.com", name: "Rômulo"}
E em seguida, vamos criar nosso Changeset:
iex> alias Ecto4noobs.User
Ecto4noobs.User
iex> User.changeset(user_params)
#Ecto.Changeset<
action: nil,
changes: %{age: 23, email: "romulo@tomate.com", name: "Rômulo"},
errors: [],
data: #Ecto4noobs.User<>,
valid?: true
>
Agora temos um Changeset do Ecto que é uma struct especial que valida os nossos dados, faz cast dos dados e vai ser mandada para o banco.
Dito isso, vamos criar as validações pela função validate_required
que também recebe uma lista.
defmodule Ecto4noobs.User do
use Ecto.Schema
import Ecto.Changeset
@required_params [:age, :email, :name]
schema "users" do
...
end
def changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, @required_params)
|> validate_required(@required_params)
end
end
Voltando para o nosso iex, vamos remover o name de user_params
e tentar criar o changeset novamente:
iex> user_params = %{email: "romulo@tomate.com", age: 23}
%{age: 23, email: "romulo@tomate.com"}
iex> User.changeset(user_params)
#Ecto.Changeset<
action: nil,
changes: %{age: 23, email: "romulo@tomate.com"},
errors: [name: {"can't be blank", [validation: :required]}],
data: #Ecto4noobs.User<>,
valid?: false
>
Perfeito! Está tudo funcionando, ele nós retorna um error
dizendo que o campo name
não pode ser vazio e um valid?
false!
Com isso, já podemos partir para a inserção de dados.
Escrita de dados
Para fazermos a escrita de dados, vamos utilizar o Ecto.Repo
que define um repositório e mapeia os dados que temos no elixir e o nosso repositório físico que é o nosso banco de dados.
Vamos ao iex criar nosso map com todos os campos preenchidos:
iex> user_params = %{name: "Rômulo", email: "romulo@tomate.com", age: 23}
%{age: 23, email: "romulo@tomate.com", name: "Rômulo"}
Agora vamos criar nosso Changeset
e em seguida vamos inserir no nosso banco de dados utilizando Repo.insert/1
:
iex> alias Ecto4noobs.User
Ecto4noobs.User
iex> alias Ecto4noobs.Repo
Ecto4noobs.Repo
iex> user_params |> User.changeset() |> Repo.insert()
15:36:36.977 [debug] QUERY OK db=4.4ms decode=1.1ms queue=1.9ms idle=1294.1ms
INSERT INTO "users" ("age","email","name") VALUES ($1,$2,$3) RETURNING "id" [23, "romulo@tomate.com", "Rômulo"]
{:ok,
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 23,
email: "romulo@tomate.com",
id: 1,
name: "Rômulo"
}}
Leitura de dados
Após ter feito a nossa escrita no banco, agora podemos também fazer a leitura de todos os dados e é bem simples, basta usarmos Repo.all/1
.
Vamos testar no iex:
iex> Repo.all(User)
15:41:33.261 [debug] QUERY OK source="users" db=1.0ms queue=1.1ms idle=1589.9ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 []
[
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 23,
email: "romulo@tomate.com",
id: 1,
name: "Rômulo"
}
]
Ou, podemos fazer a leitura utilizando o Repo.get/2
:
iex> Repo.get(User, 1)
15:42:45.240 [debug] QUERY OK source="users" db=1.0ms queue=1.6ms idle=1568.2ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 23,
email: "romulo@tomate.com",
id: 1,
name: "Rômulo"
}
O que fizemos além de passar User
foi passar o ID
do usuário que queremos listar.
Além das listagens Repo.all/1
e Repo.get/2
também é possível fazer a leitura utilizando um filtro.
Para isso vamos dizer ao módulo que queremos utilizar suas macros.
iex> require Ecto.Query
Ecto.Query
E agora vamos utilizar o filtro para buscar somente usuários que possui o nome Floki (Eu inseri outro usuário antes de fazer a listagem com filtro).
Voltando para o iex:
iex> Ecto.Query.where(User, name: "Floki") |> Repo.all()
15:51:10.119 [debug] QUERY OK source="users" db=0.7ms queue=1.4ms idle=1447.6ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 WHERE (u0."name" = 'Floki') []
[
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 3,
email: "floki@gato.com",
id: 2,
name: "Floki"
}
]
Atualização de dados
Para atualizarmos os dados de algum usuário é muito simples!
Vamos utilizar o Repo.get/2
passando User
e o ID
do usuário que queremos atualizar.
Repo.get(User, 2)
Em seguida, vamos criar um Changeset
com o campo que queremos fazer a atualização e enviar essa atualização para o banco utilizando Repo.update/1
iex> Repo.get(User, 2) |> User.changeset(%{email: "floki@gato.com"}) |> Repo.update()
15:57:31.334 [debug] QUERY OK source="users" db=1.9ms idle=1662.5ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 WHERE (u0."id" = $1) [2]
{:ok,
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 3,
email: "floki@gato.com",
id: 2,
name: "Floki"
}}
Prontinho! Nosso campo e-mail
foi atualizado com sucesso! :)
Remoção de dados
Se você achou fácil atualizar os dados, verá que para remover é muito mais simples!
Para remover um usuário, vamos utilizar o Repo.get/2
passando User
e o ID
do usuário que queremos deletar.
Repo.get(User, 1)
E por fim, vamos enviar nossa remoção para o banco utilizando Repo.delete/1
iex> Repo.get(User, 1) |> Repo.delete()
16:02:34.110 [debug] QUERY OK source="users" db=1.9ms queue=0.1ms idle=1438.4ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
16:02:34.114 [debug] QUERY OK db=2.6ms queue=1.1ms idle=1440.6ms
DELETE FROM "users" WHERE "id" = $1 [1]
{:ok,
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:deleted, "users">,
age: 23,
email: "romulo@tomate.com",
id: 1,
name: "Rômulo"
}}
Para conferir, vamos fazer a leitura de todos os dados:
iex> Repo.all(User)
16:03:08.243 [debug] QUERY OK source="users" db=1.2ms idle=1572.6ms
SELECT u0."id", u0."name", u0."email", u0."age" FROM "users" AS u0 []
[
%Ecto4noobs.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 3,
email: "floki@gato.com",
id: 2,
name: "Floki"
}
]
E é isso! Ficou apenas o usuário Floki :)
Conclusão
Muito obrigado pela leitura até aqui e espero ter ajudado de alguma forma. Tem alguma sugestão ou encontrou algum problema? por favor deixe-me saber. 💜
Código do projeto no meu github
Top comments (4)
Muito bacana Rômulo, obrigado por compartilhar! :)
É bacana perceber que tanto o design do Phoenix quanto do Ecto super combinam nesses aspectos!
Enquanto o Phoenix prega por isolar sua camada de dados entre contextos (Bounded-contexts), para garantir que eles sejam responsáveis por uma parte do domínio do seu negócio, ele também funciona para garantir que o contexto será o módulo que irá executar tudo relacionado a comunicação com o banco de dados (funções impuras).
Já o Ecto, baseado no Repository Pattern, garante que independente do que você faça com qualquer parte do toolkit dele (Query, Schema, Changeset), você só estará de fato fazendo um round-trip para o banco enviando ou requisitando dados caso use o módulo Repo (repository). Por isso, sempre costumamos usar o Repo dentro de um contexto do Phoenix em aplicações web!
Essas integrações implícitas do ecossistema do Elixir são as coisas mais lindas que existem!
Que honra receber seu comentário aqui! Obrigado demais pelo feedback 💜
Elixir é maravilhoso!
Muito bom!
Valeu meu mano. Você é o melhor! 💜