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,072