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.

Lucas Mazza

July 06, 2019
Tweet

More Decks by Lucas Mazza

Other Decks in Technology

Transcript

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

    #Ecto.Schema.Metadata<:built, "posts">, # => id: nil, # => title: ["not", "a", "title"] # => }
  3. 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
  4. 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 #=> >
  5. defmodule MyAppWeb.ClientsController do def create(conn, params) do # ... end

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

    [{ "id": 10 }] } } } %MyApp.Client{ id: 5, addresses: [ %MyApp.Address{id: 10} ] }
  7. def changeset(client, params \\ %{}) do client |> cast(params, [])

    |> cast_embed(:addresses, required: true) # ... end
  8. # 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)
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. defmodule ThirdPartyClient do @behavior APIClient def get_transactions() do request |>

    Enum.map(&TransactionParser.parse/1) |> Enum.filter(&match?(%Transaction{}, &1)) end end
  15. ✅ 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
  16. :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(%{})
  17. 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
  18. 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
  19. 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!
  20. defmodule MyApp.Service do defmacro __using__(schema) do quote do import Ecto.Changeset

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

    changeset, a schema struct or a `{data, types}` tuple.