Slide 1

Slide 1 text

Ecto Without a DB: Many Meanings of Map Greg Vaughn

Slide 2

Slide 2 text

• @gvaughn or @gregvaughn on GitHub, Twitter, Slack, ElixirForum, etc. • Elixiring since 2013 • Professionally since 2017 Who Dat

Slide 3

Slide 3 text

ORM? ORM? ORM What's Ecto?

Slide 4

Slide 4 text

Main Parts of Ecto ✔ ✔

Slide 5

Slide 5 text

iex environment $ mix phx.new ecto_without_a_db --module EctoNoDb --no-ecto Added to mix.exs in `deps/0` function: {:ecto, "~> 3.0"}, {:phoenix_ecto, "~> 4.0"} $ mix deps.get $ iex -S mix

Slide 6

Slide 6 text

Structs are maps with superpowers Every journey starts with a map

Slide 7

Slide 7 text

Anatomy of a Struct iex(1)> defmodule A, do: defstruct [a: 1, b: 2, c: 3] {:module, A, <<... iex(2)> inspect %A{} "%A{a: 1, b: 2, c: 3}" iex(3)> inspect %A{}, structs: false "%{__struct__: A, a: 1, b: 2, c: 3}" iex(4)> A.__info__(:functions) [__struct__: 0, __struct__: 1] iex(5)> inspect A.__struct__(), structs: false "%{__struct__: A, a: 1, b: 2, c: 3}"

Slide 8

Slide 8 text

Schemas are Structs with superpowers2

Slide 9

Slide 9 text

Anatomy of an Embedded Schema iex(1)> defmodule B do ...(1)> use Ecto.Schema; embedded_schema do ...(1)> field(:a, :integer); field(:b, :string); field(:c, :float) ...(1)> end ...(1)> end {:module, B, <<... iex(2)> inspect %B{}, structs: false "%{__struct__: B, a: nil, b: nil, c: nil, id: nil}" iex(3)> B.__info__(:functions) [__changeset__: 0, __schema__: 1, __schema__: 2, __struct__: 0, __struct__: 1] iex(4)> B.__schema__(:fields) [:id, :a, :b, :c] iex(5)> B.__changeset__() %{a: :integer, b: :string, c: :float, id: :binary_id}

Slide 10

Slide 10 text

Changesets are Command Objects Changesets are Command Objects Structs

Slide 11

Slide 11 text

Anatomy of a Changeset iex(1)> inspect %Ecto.Changeset{} #Ecto.Changeset< action: nil, changes: %{}, errors: [], data: nil, valid?: false > iex(2)> inspect %Ecto.Changeset{}, structs: false "%{__struct__: Ecto.Changeset, action: nil, changes: %{}, constraints: [], data: nil, empty_values: [\"\"], errors: [], filters: %{}, params: nil, prepare: [], repo: nil, repo_opts: [], required: [], types: nil, valid?: false, validations: [] }"

Slide 12

Slide 12 text

Anatomy of a Changeset iex(3)> c = Ecto.Changeset.cast(%B{a: 0}, %{a: 1, b: "Lonestar", c: 3.0}, [:a, :b]) #Ecto.Changeset< action: nil, changes: %{a: 1, b: "Lonestar"}, errors: [], data: #B<>, valid?: true > iex(4)> inspect c, structs: false "%{__struct__: Ecto.Changeset, action: nil, changes: %{a: 1, b: \"Lonestar\"}, constraints: [], empty_values: [\"\"], errors: [], filters: %{}, prepare: [], repo: nil, repo_opts: [], required: [], valid?: true, validations: [] data: %{__struct__: B, a: 0, b: nil, c: nil, id: nil}, params: %{\"a\" => 1, \"b\" => \"Lonestar\", \"c\" => 3.0}, types: %{a: :integer, b: :string, c: :float, id: :binary_id}, }"

Slide 13

Slide 13 text

Back to Struct-land iex(1)> {:ok, b} = Ecto.Changeset.apply_action(c, :update) {:ok, %B{a: 1, b: "Lonestar", c: nil, id: nil}} iex(2)> Ecto.Changeset.cast(%B{}, %{a: 3.14}, [:a, :b]) |> ...(2)> Ecto.Changeset.apply_action(:update) {:error, #Ecto.Changeset< action: :update, changes: %{}, errors: [a: {"is invalid", [type: :integer, validation: :cast]}], data: #B<>, valid?: false >}

Slide 14

Slide 14 text

Elmo loves "M" for "mapping!" Elmo loves Ecto because it rhymes with "Elmo!"

Slide 15

Slide 15 text

The Phoenix Protocols

Slide 16

Slide 16 text

Transform to Form iex(1)> {:error, c} = Ecto.Changeset.cast(%B{}, %{a: 3.14}, [:a, :b]) |> Ecto.Changeset.apply_action(:update) ... iex(2)> Phoenix.HTML.Form.form_for(c, "http://somewhere/clever") %Phoenix.HTML.Form{ action: "http://somewhere/clever", data: %B{a: nil, b: nil, c: nil, id: nil}, errors: [a: {"is invalid", [type: :integer, validation: :cast]}], hidden: [], id: "b", impl: Phoenix.HTML.FormData.Ecto.Changeset, index: nil, name: "b", options: [method: "post"], params: %{"a" => 3.14}, source: #Ecto.Changeset< action: :update, changes: %{}, errors: [a: {"is invalid", [type: :integer, validation: :cast]}], data: #B<>, valid?: false > }

Slide 17

Slide 17 text

Example: Advanced Search defmodule EctoNoDb.EventSearchController do use EctoNoDbWeb, :controller def index(conn, %{"q" => query_params}) do ... end end

Slide 18

Slide 18 text

Example: Advanced Search iex(1)> query_params = %{"name" => "BBQ", "city" => "Austin", "start" => "2019-02-28", "end" => "2019-03-03"} iex(2)> types = %{name: :string, city: :string, start: :date, end: :date} iex(3)> c = Ecto.Changeset.cast({%{}, types}, query_params, Map.keys(types)) #Ecto.Changeset< action: nil, changes: %{ city: "Austin", end: ~D[2019-03-03], name: "BBQ", start: ~D[2019-02-28] }, errors: [], data: %{}, valid?: true >

Slide 19

Slide 19 text

Example: Advanced Search iex(4)> inspect c, structs: false "%{__struct__: Ecto.Changeset, ... without fields shown above ... constraints: [], empty_values: [\"\"], filters: %{}, prepare: [], repo: nil, repo_opts: [], required: [], validations: [], params: %{\"city\" => \"Austin\", \"end\" => \"2019-03-03\", \"name\" => \"BBQ\", \"start\" => \"2019-02-28\"}, types: %{city: :string, end: :date, name: :string, start: :date} }" iex(5)> Ecto.Changeset.apply_action(c, :update) {:ok, %{city: "Austin", end: ~D[2019-03-03], name: "BBQ", start: ~D[2019-02-28]}}

Slide 20

Slide 20 text

Example: Advanced Search defmodule EctoNoDb.EventSearchController do use EctoNoDbWeb, :controller @query_types = %{name: :string, city: :string, start: :date, end: :date} def index(conn, %{"q" => query_params}) do with {:ok, query} <- cast_params(query_params, @query_types), {:ok, results} <- NotInScopeForThisTalk.get_results(query) do ... do your thang ... end end defp cast_params(query_params, type_map) do {%{}, type_map} |> Ecto.Changeset.cast(query_params, Map.keys(type_map)) |> Ecto.Changeset.apply_action(:update) end end

Slide 21

Slide 21 text

Anti-Corruption Layer “If your application needs to deal with a database or another application whose model is undesirable or inapplicable to the model you want within your own application, use an AnticorruptionLayer to translate to/from that model and yours.” – c2 wiki

Slide 22

Slide 22 text

Anti-Corruption Layer API Client Anti Corruption Layer Domain Logic Typed Struct

Slide 23

Slide 23 text

Anti-Corruption Layer defmodule EctoNoDb.Event do use Ecto.Schema import Ecto.Changeset embedded_schema do field(:name, :string) field(:venue_name, :string) field(:venue_city, :string) field(:occurs_at, :naive_datetime) end def new(raw_map) do %__MODULE__{} |> cast(raw_map, [:name, :venue_name, :venue_city, :occurs_at]) |> apply_action(:update) end end

Slide 24

Slide 24 text

Anti-Corruption Layer defmodule EctoNoDb.Event do use Ecto.Schema import Ecto.Changeset embedded_schema do field(:name, :string) field(:occurs_at, :naive_datetime) embeds_one(:venue, EctoNoDb.Venue) embeds_one(:performer, EctoNoDb.Performer) end def new(raw_map) do %__MODULE__{} |> cast(raw_map, [:name, :occurs_at]) |> cast_embed(:venue) |> cast_embed(:performer) |> apply_action(:update) end end

Slide 25

Slide 25 text

Anti-Corruption Layer defmodule EctoNoDb.Event do ... def new(raw_map) do %__MODULE__{} |> changeset(raw_map) |> apply_action(:update) end defp changeset(base, raw_map) do base |> cast(raw_map, [:name, :occurs_at]) |> cast_embed(:venue) |> cast_embed(:performer) end ... end

Slide 26

Slide 26 text

Meta-Anti-Corruption Layer defmodule EctoNoDb.Event do use EctoNoDb.AntiCorruptionSchema embedded_schema do field(:name, :string) field(:occurs_at, :naive_datetime) embeds_one(:venue, EctoNoDb.Venue) embeds_one(:performer, EctoNoDb.Performer) end end defmodule EctoNoDb.Venue do use EctoNoDb.AntiCorruptionSchema embedded_schema do field(:name, :string) field(:city, :string) end end

Slide 27

Slide 27 text

Meta-Anti-Corruption Layer defmodule EctoNoDb.AntiCorruptionSchema do defmacro __using__(_opts) do quote do use Ecto.Schema @primary_key false def new(raw_map), do: unquote(__MODULE__).generic_new(raw_map, __MODULE__) def changeset(base, params), do: unquote(__MODULE__).generic_changeset(base, params) defoverridable new: 1, changeset: 2 end end def generic_new(raw_map, struct_module) do struct(struct_module) |> struct_module.changeset(raw_map) |> Ecto.Changeset.apply_action(:update) end def generic_changeset(base, raw_map) do struct_mod = base.__struct__ embeds = struct_mod.__schema__(:embeds) allowed = struct_mod.__schema__(:fields) -- embeds chg = Ecto.Changeset.cast(base, raw_map, allowed) Enum.reduce(embeds, chg, &Ecto.Changeset.cast_embed(&2, &1)) end end

Slide 28

Slide 28 text

Thank You • Takeaways • Maps -> Structs -> Schemas -> Changesets -> Structs/Maps • Understanding one level below where you are focused • Ideas for when to use typed structs in your domain • [email protected] • Twitter: @gregvaughn • Elixir Slack: @gregvaughn • Elixir Forum: gregvaughn • Questions?