Slide 1

Slide 1 text

ecto sem sql a teoria na prática

Slide 2

Slide 2 text

@lucasmazza https://afterhours.io

Slide 3

Slide 3 text

https://magnetis.workable.com

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

“Ecto is a toolkit for data mapping and language integrated query for Elixir.”

Slide 6

Slide 6 text

Ecto.Changeset Ecto.Schema Ecto.Repo Ecto.Query

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

use Ecto.Schema

Slide 9

Slide 9 text

use Ecto.Schema defstruct melhorado

Slide 10

Slide 10 text

use Ecto.Schema defstruct melhorado schema/2 e embedded_schema/1

Slide 11

Slide 11 text

use Ecto.Schema defstruct melhorado schema/2 e embedded_schema/1 Não valida/transforma nada

Slide 12

Slide 12 text

defmodule MyApp.Post do use Ecto.Schema schema "posts" do field :title, :string end end %MyApp.Post{} # => %MyApp.Post{ # => __meta__: … # => id: nil, # => title: nil # => } MyApp.Post.__info__(:functions) # => [ # => __changeset__: 0, # => __schema__: 1, # => __schema__: 2, # => __struct__: 0, # => __struct__: 1 # => ]

Slide 13

Slide 13 text

%MyApp.Post{title: ["not", "a", "title"]} # => %MyApp.Post{ # => __meta__: #Ecto.Schema.Metadata<:built, "posts">, # => id: nil, # => title: ["not", "a", "title"] # => }

Slide 14

Slide 14 text

import Ecto.Changeset

Slide 15

Slide 15 text

import Ecto.Changeset API de validações

Slide 16

Slide 16 text

import Ecto.Changeset API de validações Estrutura de dados para mudanças

Slide 17

Slide 17 text

import Ecto.Changeset API de validações Estrutura de dados para mudanças Criado via change/2 e cast/4

Slide 18

Slide 18 text

import Ecto.Changeset API de validações Estrutura de dados para mudanças Criado via change/2 e cast/4 Convenção do changeset/2

Slide 19

Slide 19 text

post = %MyApp.Post{title: "original"} Ecto.Changeset.change(post, %{title: "changed"}) Ecto.Changeset.cast(post, %{title: "changed"}, [:title]) #=> #Ecto.Changeset< #=> action: nil, #=> changes: %{title: "changed"}, #=> errors: [], #=> data: #MyApp.Post<>, #=> valid?: true #=> >

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

#1 - documentos JSON:API

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

defmodule MyAppWeb.ClientsController do def create(conn, params) do # ... end end Um Map com outro Map com outro Map com outro..…

Slide 24

Slide 24 text

{ "data": { "type": "clients", "id": 5, "attributes": { "addresses": [{ "id": 10 }] } } } %MyApp.Client{ id: 5, addresses: [ %MyApp.Address{id: 10} ] }

Slide 25

Slide 25 text

defmodule MyApp.Client do use Ecto.Schema import Ecto.Changeset embedded_schema do # ... embeds_many :addresses, MyApp.Address end end

Slide 26

Slide 26 text

def changeset(client, params \\ %{}) do client |> cast(params, []) |> cast_embed(:addresses, required: true) # ... end

Slide 27

Slide 27 text

def deserialize(params) do %__MODULE__{} |> changeset(params) |> apply_action(:insert) end

Slide 28

Slide 28 text

# apply_action/2 # If the changes are valid, all changes are applied to the changeset data. # If the changes are invalid, no changes are applied, and an error tuple # is returned with the changeset containing the action that was attempted # to be applied. {:ok, data} = apply_action(changeset, :insert) {:error, changeset} = apply_action(changeset, :update) # yolo data = apply_changes(changeset)

Slide 29

Slide 29 text

defmodule MyAppWeb.ClientsController do def create(conn, %{"data" => %{"id" => id, "attributes" => data}}) do with {:ok, client} <- MyApp.Client.deserialize(Map.put(data, "id", id)), {:ok, response} <- MyApp.ContextXPTO.call(client) do conn |> put_status(:no_content) |> send_resp(204, "") end end end

Slide 30

Slide 30 text

Tipagem e validações de sempre

Slide 31

Slide 31 text

Tipagem e validações de sempre MyApp.ContextXPTO não se importa

Slide 32

Slide 32 text

Tipagem e validações de sempre MyApp.ContextXPTO não se importa O seu ChangesetView ainda funciona

Slide 33

Slide 33 text

#2 - APIs externas

Slide 34

Slide 34 text

XML “REST” SOAP

Slide 35

Slide 35 text

XML “REST” SOAP

Slide 36

Slide 36 text

Terceiros Sua app Elixir

Slide 37

Slide 37 text

Terceiros Sua app Elixir

Slide 38

Slide 38 text

Terceiros Sua app Elixir Contratos Explícitos

Slide 39

Slide 39 text

defmodule ThirdPartyClient do @behavior APIClient def get() do request |> format() # Traduzir estruturas de dados externas |> cast() # Transformar em entidades do sistema |> reject() # Tratar dados inválidos end end

Slide 40

Slide 40 text

defmodule Transaction do use Ecto.Schema import Ecto.Changeset embedded_schema do field(:value, :decimal) end end

Slide 41

Slide 41 text

def new(params) do %__MODULE__{} |> changeset(params) |> apply_action(:insert) end def changeset(transaction, params) do transaction |> cast(params, [:id, :value]) |> validate_required([:id, :value]) end

Slide 42

Slide 42 text

defmodule ThirdParty.TransactionsParser do @attributes %{ Id: :id, Valor: :value } end

Slide 43

Slide 43 text

defmodule ThirdParty.TransactionsParser do @attributes %{ Id: :id, Valor: :value } def parse(data) do data = translate_keys(data, @attributes) end defp translate_keys(data, mapping) do data |> Map.take(Map.keys(mapping)) |> Map.new(fn {key, value} -> {Map.fetch!(mapping, key), value} end) end end

Slide 44

Slide 44 text

defmodule ThirdParty.TransactionsParser do @attributes %{ Id: :id, Valor: :value } def parse(data) do data = translate_keys(data, @attributes) case Transaction.new(data) do {:ok, transaction} -> transaction {:error, changeset} -> {:error, changeset} end end defp translate_keys(data, mapping) do data |> Map.take(Map.keys(mapping)) |> Map.new(fn {key, value} -> {Map.fetch!(mapping, key), value} end) end end

Slide 45

Slide 45 text

defmodule ThirdPartyClient do @behavior APIClient def get_transactions() do request |> Enum.map(&TransactionParser.parse/1) |> Enum.filter(&match?(%Transaction{}, &1)) end end

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

✅ Schemas como a representação ideal

Slide 48

Slide 48 text

✅ Schemas como a representação ideal Módulos colaboradores podem assumir que os dados estão OK

Slide 49

Slide 49 text

✅ Schemas como a representação ideal Módulos colaboradores podem assumir que os dados estão OK Parsers/Formatters/etc específicos para domínios externos

Slide 50

Slide 50 text

#3 - contextos https:/ /www.amberbit.com/blog/2017/12/27/ecto-as-elixir-data-casting-and-validation-library/

Slide 51

Slide 51 text

Domínio B Domínio A Struct

Slide 52

Slide 52 text

Hash/Map Driven Development?

Slide 53

Slide 53 text

:ok = ContextA.submit(%{a_param: "Hello", another_param: "10"}) :ok = ContextA.submit(%{a_param: "Goodbye", another_param: 30, boolean_param: false}) {:error, error} = MyApp.ContextA.submit(%{})

Slide 54

Slide 54 text

defmodule MyApp.ContextA do use MyApp.Service, %{ a_param: :string, another_param: :decimal, boolean_param: :boolean } end

Slide 55

Slide 55 text

defmodule MyApp.ContextA do use MyApp.Service, %{ a_param: :string, another_param: :decimal, boolean_param: :boolean } defp validate(changeset) do changeset |> validate_required([:a_param, :another_param]) end end

Slide 56

Slide 56 text

defmodule MyApp.ContextA do use MyApp.Service, %{ a_param: :string, another_param: :decimal, boolean_param: :boolean } def submit(params) do case process_params(params) do {:ok, data} -> # algo importante com `data`... :ok error -> error end end defp validate(changeset) do changeset |> validate_required([:a_param, :another_param]) end end

Slide 57

Slide 57 text

defmodule MyApp.ContextA do use MyApp.Service, %{ a_param: :string, another_param: :decimal, boolean_param: :boolean } def submit(params) do case process_params(params) do {:ok, data} -> # algo importante com `data`... :ok error -> error end end defp validate(changeset) do changeset |> validate_required([:a_param, :another_param]) end end Olha só, sem Ecto.Schema!

Slide 58

Slide 58 text

defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset end end end

Slide 59

Slide 59 text

defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset defp validate(changeset), do: changeset defoverridable [validate: 1] end end end

Slide 60

Slide 60 text

defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset defp validate(changeset), do: changeset defoverridable [validate: 1] defp process_params(params) do params |> cast() |> validate() |> apply_action(:insert) end end end end

Slide 61

Slide 61 text

defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset defp validate(changeset), do: changeset defoverridable [validate: 1] defp process_params(params) do params |> cast() |> validate() |> apply_action(:insert) end defp cast(params) do types = Enum.into(unquote(schema), %{}) permitted = Map.keys(types) data = Map.new(permitted, fn prop -> {prop, nil} end) Ecto.Changeset.cast({data, types}, params, permitted) end end end end

Slide 62

Slide 62 text

types = Enum.into(unquote(schema), %{}) # => %{a_param: :string, another_param: :decimal, boolean_param: :boolean} permitted = Map.keys(types) # => [:a_param, :another_param, :boolean_param] data = Map.new(permitted, fn prop -> {prop, nil} end) # => %{a_param: nil, another_param: nil, boolean_param: nil} Ecto.Changeset.cast({data, types}, params, permitted)

Slide 63

Slide 63 text

{:error, changeset} = MyApp.ContextA.submit(%{}) Ecto.Changeset.traverse_errors(changeset, fn {msg, _} -> msg end) # => %{a_param: ["can't be blank"], another_param: ["can't be blank"]} :ok = MyApp.ContextA.submit(%{a_param: "Hello", another_param: "10"})

Slide 64

Slide 64 text

h Ecto.Changeset.cast/4 # The given `data` may be either a changeset, a schema struct or a `{data, types}` tuple.

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

“Ecto is a toolkit for data mapping and language integrated query for Elixir.”

Slide 67

Slide 67 text

Ecto.Changeset & Ecto.Schema

Slide 68

Slide 68 text

https://hexdocs.pm/ecto

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

obrigado! @lucasmazza https:/ /speakerdeck.com/lucas tirinhas do https:/ /garfieldminusgarfield.net