DEV Community

Rômulo Silva
Rômulo Silva

Posted on

Introdução ao Ecto

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
>
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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"
 }}
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
 }}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
 }}
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
wlsf profile image
Willian Frantz

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!

Collapse
 
rohlacanna profile image
Rômulo Silva • Edited

Que honra receber seu comentário aqui! Obrigado demais pelo feedback 💜

Elixir é maravilhoso!

Collapse
 
teijiw profile image
Teiji

Muito bom!

Collapse
 
rohlacanna profile image
Rômulo Silva

Valeu meu mano. Você é o melhor! 💜