Simple Ectonomy

Mafinar Khan ◦ Planswell ◦ @mafinar

Tonight

  • Zero to Ecto
  • When the glass ain't empty
  • Working with Legacy
  • Ecto and the Domain

zero to ecto

how do we work with data?

define? search? write?

thought experiment

you only know elixir

remember that cs101 project that resembled mis?

every programming language, ever!

  • Ability to define structures
  • Ability to compose structures
  • Ability to operate on structures
  • Ability to name structures

elixir way

  • Modules
  • Structs/Maps/Lists
  • Pipes
  • Immutability
  • Functional Programming
  • ...

The Data Way

  • Create
  • Query
  • Transform
  • Save
  • Load

what about persistance?

all we know is elixir for now

nice to do

  • Data as Structs
  • Transformation through Pipes
  • Stage all the changes
  • Delay touching the source and sink as much as possible
  def vehicle_locations(params) do
    with {:ok, vehicles} <- ClientManager.vehicles_for_group(params) do
      case vehicles
           |> Enum.map(&transform_into_map/1)
           |> Enum.map(&fetch_locations/1)
           |> Enum.filter(&(&1 != nil))
           |> Enum.map(&populate_landmark/1) do
        [] -> {:error, :no_location}
        locations -> {:ok, locations}
      end
    else
      {:error, _} -> {:error, :no_vehicles}
    end

enter ecto

elixir + data(bases)

In Ecto

  • Migration (Ecto.Migration)
  • Schema (Ecto.Schema)
  • Changeset (Ecto.Changeset)
  • Query (Ecto.Query)
  • Repo (Ecto.Repo)

Schema

represent your data

defmodule Wltx.Accounts.User do
  @moduledoc """
  Module for the authentication table and abstraction 
  of the Django user model
  """
  use Ecto.Schema

  schema "setup_appuser" do
    field(:username, :string)
    field(:password, :string)
    field(:first_name, :string)
    field(:last_name, :string)
    field(:email, :string)
    field(:is_superuser, :boolean)
    field(:client_group_id, :integer)
    field(:tags, {:array, :integer}, virtual: true)
  end
end

schemas are...

  • May or may not map to database table
  • Fields may or may not be table fields
  • Types can be customized
  • Associate with other Schemas
  • Ecto can live without them

query

searching the structures

from(
        loc in related_table,
        where: loc.lat > 0 and loc.lng > 0,
        group_by: [
          loc.sysdatetime,
          loc.lat,
          loc.lng,
          loc.speed,
          loc.gpstat,
          loc.courseval,
          loc.engine
        ],
        select: %{
          vehicle: fragment("? ::integer", ^id),
          actual_time: loc.sysdatetime,
          latest_time: fragment("max(sysdatetime)"),
          lat: loc.lat,
          lng: loc.lng,
          speed: fragment("case when max(sysdatetime) = sysdatetime then speed else 0 end"),
          gpstat: loc.gpstat,
          courseval: loc.courseval,
          time_delta:
            fragment("extract ('epoch' from (max(sysdatetime) - sysdatetime)::interval)"),
          engine: loc.engine
        },
        limit: 1
      )

changeset

create and change

Changesets

  • Responsible for marrying struct with incoming data
  • Casting, Validation and Transform
  • Operations/Result As Data
  • Stores Results and Errors
  • Think Git

Title Text

  def signup(struct, params \\ %{}) do
    struct
      |> cast(params, [:email, :password, :password_confirmation])
      |> validate_required([email, :password, :password_confirmation])
      |> validate_format(:email, ~r/@/)
      |> validate_length(:password, min: 5)
      |> password_and_confirmation_matches()
      |> generate_password_hash()
  end


  defp password_and_confirmation_matches(changeset) do
    password = get_change(changeset, :password)
    password_confirmation = get_change(changeset, :password_confirmation)
    if password == password_confirmation do
      changeset
    else
      changeset
        |> add_error(:password_confirmation, 
                     "password_confirmation does not match password!")
    end
  end

  defp generate_password_hash(changeset) do
    password = get_change(changeset, :password)
    hash = Comeonin.Bcrypt.hashpwsalt(password)
    changeset |> put_change(:password_hash, hash)
  end

Changesets (cont)

  • Struct in Struct Out
  • Think Middleware
  • Keep transforming till you hit Repository

Repo

finally the database

repo

  • The interface between data store and data
  • The final endpoint before the side-effect
  • Can be overridden to meet additional functions

when the glass ain't empty

ecto is not your regular orm

ecto principles

  • Explicit is better than implicit
  • Data, not database is the main focus
  • Lazy Loading is not a feature (No more N+1?)
  • Data Structures!
  • Repository Pattern
  • Database is not of concern till Repo is hit
  • It's just Elixir

if you know an orm

  • Schemas are definitions, they don't save themselves
  • Schemas don't magically map with database
  • DRY can sometimes cause dehydration
  • Separation of Concern when Saving, Loading or Defining data
  • Changesets can be difficult to wrap head around

working with legacy

when you have databases from before

working with legacy database

  • Naming convention can be tweaked
  • Primary/Foreign Key can be configured idiomatically
  • As last resort, types can be introduced
  • Multiple Data Source? Multiple Repos
  • Fragments are Awesome
  • Schemaless Queries or Changesets are great

Ecto and the domain

ecto in ddd

  • Multiple Schema per Table and Tableless Schema can form Domain Object (?)
  • Multiple Schemas can easily be joined into Elixir Struct, or Aggregates
  • Changeset and Multi can enable Event Sourcing
  • All of the above can take formation inside GenServer and enable Domain Events
  • Remember, you don't really need Schema, and Query can hydrate any composite data

thank you

Simple Ectonomy

By Mafinar Khan

Simple Ectonomy

My talk for Toronto Elixir meetup, January 2019 edition.

  • 1,077