Elixir - Como fazer determinadas operações com a biblioteca Ecto (Parte 1)

wesleymorais

Wesley de Morais

Posted on January 27, 2023

Elixir - Como fazer determinadas operações com a biblioteca Ecto (Parte 1)

Requisitos

  • Elixir 1.14.2
  • Erlang/otp 25

Introdução

O Ecto é uma biblioteca feita em elixir que nos possibilita ter uma Api, assim como uma abstração para fazer operações em diferentes tipos de bancos de dados. Por meio dele podemos fazer uso de drives de banco de dados como Mysql(Myxql) e Postgresql(Postgrex).

Esta biblioteca se utiliza de um padrão chamado Repository, no qual todo e qualquer acesso ao banco de dados se dará por meio de funções de um determinado repositório criado. O Ecto é dividido em 4 componentes principais:

  • Ecto.Repo
    • Responsável por ser o Repositório de acesso ao banco, ou seja, operações de busca(Query), Inserção, atualização e remoção de dados do banco de dados será por meio de funções dele.
  • Ecto.Query
    • Responsável por ter funções de criação de Query, ou seja, ler do banco de dados alguma informação.
  • Ecto.Schema
    • Responsável por mapear determinados dados externos para estruturas elixir.
  • Ecto.Changeset
    • Responsável por validar e filtrar determinados dados recebidos, afim de acessar o banco de dados.

Situação problema

Nada melhor do que entender uma nova tecnologia do que em uma situação problema que você vai aplica-la.

Imagine que um determinado proprietário de uma pizzaria tenha apenas o interesse de manter os dados dos clientes, assim como as pizzas que ele poderá fazer e os pedidos desses clientes para determinadas pizzas existentes.

Podemos mapear um modelo relacional dessa maneira:

mapeamento

Mão na massa

Criando o projeto

Primeiramente vamos criar o nosso projeto usando a ferramenta Mix, então digite no terminal:

mix new pizzaria_ecto --sup
Enter fullscreen mode Exit fullscreen mode

Caso você nunca criou um projeto com o mix, aqui estamos apenas gerando uma estrutura inicial e adicionando um supervisor na nosso projeto, precisamos desse supervisor, pois a nossa aplicação com ecto será um processo e caso ocorra algum erro nele, por alguma razão, então o nosso supervisor irá reiniciar o nosso processo do inicio(let it crash 🥳).

Instalando as dependências

Antes de a gente trabalharmos com o ecto temos que ter essa biblioteca no nosso projeto, então vamos adicionar essas duas dependências na função deps dentro do arquivo mix.exs:

defp deps do
    [
      {:postgrex, "~> 0.16.5"},
      {:ecto_sql, "~> 3.9"}
    ]
  end
Enter fullscreen mode Exit fullscreen mode

Aqui estamos usando o drive do postgresql e estamos fazendo uso de uma biblioteca ecto_sql que tem como dependência o ecto, mas adiciona outras funcionalidades para trabalharmos com banco de dados relacionais. Para instalar e compilar as aplicações devemos rodar o seguinte comando no terminal:

mix do deps.get, compile
Enter fullscreen mode Exit fullscreen mode

Criando o nosso próprio repositório

Dentro do diretório pizzaria_ecto que foi criado poderemos criar um arquivo chamado repo.ex, com seguinte código:

defmodule PizzariaEcto.Repo do
  use Ecto.Repo, otp_app: :pizzaria_ecto, adapter: Ecto.Adapters.Postgres
end
Enter fullscreen mode Exit fullscreen mode

Aqui criamos um módulo que chamamos de PizzariaEcto.Repo no qual ele faz uso do Ecto.Repo, eu gosto de pensar que quando usamos o “use” fazemos com que o nosso módulo seja um módulo Ecto.Repo . Além disso temos a keyword :otp_app que nos diz quais configurações vamos usar dentro de um arquivo chamado config.exs(Vamos criar ainda), e por último termos a keyword :adapter que dizemos para o ecto que vamos usar o banco de dados Postgresql. Agora vamos criar um diretório chamado config(Na raiz do projeto) e dentro dele vamos criar um arquivo config.exs com seguinte código:

import Mix.Config


config :pizzaria_ecto, ecto_repos: [PizzariaEcto.Repo]

config :pizzaria_ecto, PizzariaEcto.Repo,
  database: "pizzaria_ecto",
  username: "postgres", # Username "postgres" is stardard user in pgdb
  password: "postgres", # Password "postgres" is our password when created pg container
  hostname: "localhost" # We expose our port when created our pg container
Enter fullscreen mode Exit fullscreen mode

Na código acima estamos fazendo algumas configurações para o nosso querido ecto, no qual temos o uso da macro config que setamos qual o nome da configuração, veja que usamos o mesmo atom usado na keyword :opt_app no módulo PizzariaEcto.Repo, além disso temos uma keyword dizendo quais serão os repositórios no nosso projeto, poderiamos ter mais de um acessando diferentes banco de dados.

Na segunda chamada a macro config temos as informações necessárias para o adapter acessar o banco de dados rodando. Opps, ainda não subimos um container postgresql, então vamos fazer isso.

Subindo container postgresql

No diretório home do projeto vamos criar um arquivo docker-compose.yml com seguinte código:

version: '3.9'

services:
  db:
    container_name: "pg_pizzaria_ecto"
    image: postgres:latest
    environment:
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres-db:/var/lib/postgresql/data
    ports:
      - '5432:5432'

volumes:
  postgres-db:
Enter fullscreen mode Exit fullscreen mode

Posteriormente vamos subir o container com seguinte comando no terminal:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Criando Schemas

Uma das formas de mapear determinados dados externos para estruturas elixir é usando o Ecto.Schema, com elas é possível fazer operações no banco de dados com retornos em estruturas elixir específicas. Dentro do diretório pizzaria_ecto crie um arquivo chamado client.ex com o seguinte conteúdo:

defmodule PizzariaEcto.Client do
  use Ecto.Schema
  alias PizzariaEcto.{Order}
  schema "clients" do
    field :name, :string
    field :phone, :string
      timestamps() # field :updated_at, :naive_datetime
                                 # field :inserted_at, :naive_datetime 

    has_many :order, Order
  end
end
Enter fullscreen mode Exit fullscreen mode

Aqui temos um módulo que é um Ecto.Schema, assim algumas funções e macros podem ser chamadas dentro do módulo que ele é especificado, um destes macros é a schema que recebe como argumento a tabela no banco de dados, assim ele vai mapear determinados campos para uma tabela no banco de dados, porém ainda não será criada as tabelas no banco de dados.

Associação Um para Muitos

Algo curioso que podemos ver no schema de PizzariaEcto.Client é a macro has_many, aqui estamos tentando mapear Um cliente podendo fazer vários pedidos, porém para ter o “casamento” deste relacionamento é necessário criar o segundo schema, então crie um arquivo chamado order.ex com o conteúdo:

defmodule PizzariaEcto.Order do
  use Ecto.Schema
  alias PizzariaEcto.{Client, Pizza, OrderPizza}

  schema "orders" do
    field :price, :float
    timestamps()

    belongs_to  :client, Client
    many_to_many :pizza, Pizza, join_through: OrderPizza
  end
end
Enter fullscreen mode Exit fullscreen mode

No código acima fizemos a mesma coisa do código anterior, mapeamos determinados campos com uma tabela no banco de dados, entretanto temos a outra parte do primeiro relacionamento entre Cliente e Order, em que usamos a macro belongs_to, com esse relacionamento estabelecido é esperado que tenha uma coluna na tabela orders chamada client_id.

Associação Muitos para Muitos

Um outro relacionamento que precisamos mapear é entre Order e Pizza, como é uma associação muito para muitos é necessário a criação de uma terceira tabela que vai fazer o papel de guardar order_id e pizza_id, assim é necessário usar a macro many_to_many em ambos os schemas e usar a keyword join_through mostrando qual o schema será usado. Vamos criar o schema de pizza, então crie um arquivo chamado pizza.ex, com o conteúdo:

defmodule PizzariaEcto.Pizza do
  use Ecto.Schema
  alias PizzariaEcto.{Order,OrderPizza}

  schema "pizzas" do
    field :name, :string
    timestamps()

    many_to_many :order, Order, join_through: OrderPizza
  end

end
Enter fullscreen mode Exit fullscreen mode

Vamos também criar o schema OrderPizza, então crie o arquivo order_pizza.ex.

defmodule PizzariaEcto.OrderPizza do
  use Ecto.Schema
  alias PizzariaEcto.{Pizza, Order}

  schema "orders_pizzars" do
    timestamps()
    belongs_to  :order, Order
    belongs_to  :pizza, Pizza

  end
end
Enter fullscreen mode Exit fullscreen mode

Ótimo, com esses schemas criados poderemos utiliza-los para fazer determinadas operações no banco de dados, mas com a criação desses ainda não estão criadas as tabelas no banco de dados, para isso é necessário criar as migrações, então vamos lá.

Criando migrations

Tabela de Cliente

O Ecto se utiliza de um componente chamado Ecto.Migrations para fazer as migrações necessárias, então para cria-la podemos usar um linha de comando no terminal, então digite:

mix ecto.gen.migrations add_clients_table
Enter fullscreen mode Exit fullscreen mode

O código acima vai criar um arquivo dentro do seguinte path priv/SEU_REPO/migrations/ID_ALEATÓRIO_add_clients_table , como eu já mencionei podemos ter vários repositórios, então este SEU_REPO significa qual repositório será criado a migração, porém como existe apenas um, então será ele. O conteúdo do arquivo criado, será o seguinte:

defmodule PizzariaEcto.Repo.Migrations.AddClientsTable do
  use Ecto.Migration

  def change do
  end
end
Enter fullscreen mode Exit fullscreen mode

Esta função change será a chamada pelo Ecto para fazer a migração, então vamos adicionar a nossa tabela:

defmodule PizzariaEcto.Repo.Migrations.AddClientsTable do
  use Ecto.Migration

  def change do
    create table("clients") do
      add :name, :string, null: false
      add :phone, :string, null: false
      timestamps()
    end

    create index("clients", :name)
  end
end
Enter fullscreen mode Exit fullscreen mode

Temos aqui a macro create que recebe o que será criado no banco e um bloco de código, então estamos criando a tabela clients com algumas colunas, podemos perceber que o nome aqui é o mesmo que colocamos no schema PizzariaEcto.Client. Além da tabela poderemos criar index para maximizar as buscas por uma coluna no banco. Podemos criar agora a tabela de Order.

Tabela de Order

mix ecto.gen.migrations add_orders_table
Enter fullscreen mode Exit fullscreen mode

Criado o arquivo priv/SEU_REPO/migrations/ID_ALEATÓRIO_add_orders_table

defmodule PizzariaEcto.Repo.Migrations.AddOrdersTable do
  use Ecto.Migration

  def change do
    create table("orders") do
      add :price, :float, null: false
      add :client_id, references("clients",  on_delete: :nothing)
      timestamps null: true
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Veja que pelo fato de termos uma associação de 1 para muitos entre Cliente e Order , então é necessário que orders tenha a foreign_key, então podemos adicionar usando o tipo references.

Tabela de Pizza

mix ecto.gen.migrations add_pizzas_table
Enter fullscreen mode Exit fullscreen mode
defmodule PizzariaEcto.Repo.Migrations.AddPizzasTable do
  use Ecto.Migration

  def change do
    create table("pizzas") do
      add :name, :string, null: false

      timestamps null: true
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

No código acima criamos a migration de pizza, mas podemos voltar no schema de Pizza e Order, e ver que eles tem uma relação de muitos para muitos entre eles, e criamos um schema OrderPizza para receber as foreign_keys, então vamos criar a tabela orders_pizzas que mapeamos no schema OrderPizza.

mix ecto.gen.migrations add_orders_pizzas_table
Enter fullscreen mode Exit fullscreen mode
defmodule PizzariaEcto.Repo.Migrations.AddOrdersPizzasTable do
  use Ecto.Migration

  def change do
    create table("orders_pizzas") do
      add :order_id, references("orders")
      add :pizza_id, references("pizzas")
      timestamps null: true
    end

    create index(:orders_pizzas, :order_id)
    create index(:orders_pizzas, :pizza_id)
  end
end
Enter fullscreen mode Exit fullscreen mode

OK, todas as migrations foram criadas até agora, mas é necessário executar a migração, então vamos digitar no terminal:

mix ecto.migrate
Enter fullscreen mode Exit fullscreen mode

E done!

Vamos para parte 2:
Segunda Parte do Artigo

Referências

Ecto Site Oficial

Programming Ecto - Build Database Apps in Elixir for Scalability and Performance

💖 💪 🙅 🚩
wesleymorais
Wesley de Morais

Posted on January 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related