Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ecto sem SQL

Ecto sem SQL

Um dos objetivos do design do ecto é ser um "toolkit para mapeamento de dados" independente do uso de um banco de dados por de trás dos panos, e nesta palestra vamos ver alguns casos de uso de schemas e changesets em situações diferentes de aplicações onde um banco de dados não está presente mas o ecto nos ajuda a resolver problemas no dia a dia.

5a90a67fa1a92e6a4b605cfd8da5e375?s=128

Lucas Mazza

July 06, 2019
Tweet

Transcript

  1. ecto sem sql a teoria na prática

  2. @lucasmazza https://afterhours.io

  3. https://magnetis.workable.com

  4. None
  5. “Ecto is a toolkit for data mapping and language integrated

    query for Elixir.”
  6. Ecto.Changeset Ecto.Schema Ecto.Repo Ecto.Query

  7. None
  8. use Ecto.Schema

  9. use Ecto.Schema defstruct melhorado

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

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

  12. 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 # => ]
  13. %MyApp.Post{title: ["not", "a", "title"]} # => %MyApp.Post{ # => __meta__:

    #Ecto.Schema.Metadata<:built, "posts">, # => id: nil, # => title: ["not", "a", "title"] # => }
  14. import Ecto.Changeset

  15. import Ecto.Changeset API de validações

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

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

    Criado via change/2 e cast/4
  18. 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
  19. 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 #=> >
  20. None
  21. #1 - documentos JSON:API

  22. None
  23. defmodule MyAppWeb.ClientsController do def create(conn, params) do # ... end

    end Um Map com outro Map com outro Map com outro..…
  24. { "data": { "type": "clients", "id": 5, "attributes": { "addresses":

    [{ "id": 10 }] } } } %MyApp.Client{ id: 5, addresses: [ %MyApp.Address{id: 10} ] }
  25. defmodule MyApp.Client do use Ecto.Schema import Ecto.Changeset embedded_schema do #

    ... embeds_many :addresses, MyApp.Address end end
  26. def changeset(client, params \\ %{}) do client |> cast(params, [])

    |> cast_embed(:addresses, required: true) # ... end
  27. def deserialize(params) do %__MODULE__{} |> changeset(params) |> apply_action(:insert) end

  28. # 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)
  29. 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
  30. Tipagem e validações de sempre

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

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

    seu ChangesetView ainda funciona
  33. #2 - APIs externas

  34. XML “REST” SOAP

  35. XML “REST” SOAP

  36. Terceiros Sua app Elixir

  37. Terceiros Sua app Elixir

  38. Terceiros Sua app Elixir Contratos Explícitos

  39. 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
  40. defmodule Transaction do use Ecto.Schema import Ecto.Changeset embedded_schema do field(:value,

    :decimal) end end
  41. 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
  42. defmodule ThirdParty.TransactionsParser do @attributes %{ Id: :id, Valor: :value }

    end
  43. 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
  44. 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
  45. defmodule ThirdPartyClient do @behavior APIClient def get_transactions() do request |>

    Enum.map(&TransactionParser.parse/1) |> Enum.filter(&match?(%Transaction{}, &1)) end end
  46. None
  47. ✅ Schemas como a representação ideal

  48. ✅ Schemas como a representação ideal Módulos colaboradores podem assumir

    que os dados estão OK
  49. ✅ 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
  50. #3 - contextos https:/ /www.amberbit.com/blog/2017/12/27/ecto-as-elixir-data-casting-and-validation-library/

  51. Domínio B Domínio A Struct

  52. Hash/Map Driven Development?

  53. :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(%{})
  54. defmodule MyApp.ContextA do use MyApp.Service, %{ a_param: :string, another_param: :decimal,

    boolean_param: :boolean } end
  55. 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
  56. 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
  57. 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!
  58. defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset

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

    defp validate(changeset), do: changeset defoverridable [validate: 1] end end end
  60. 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
  61. 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
  62. 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)
  63. {: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"})
  64. h Ecto.Changeset.cast/4 # The given `data` may be either a

    changeset, a schema struct or a `{data, types}` tuple.
  65. None
  66. “Ecto is a toolkit for data mapping and language integrated

    query for Elixir.”
  67. Ecto.Changeset & Ecto.Schema

  68. https://hexdocs.pm/ecto

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