Ecto: Elixir database wrapper

By Thomas Bracher

Elixir lang

  1. Based on Erlang VM
  2. Immutability, process-oriented, functional...
  3. And: MACRO
  4. Syntax similar to ruby

Basic types

string = "hello"
atom = :some_atom
tuple = {"nice", "tuple"}
map = %{some: "other", :structure => 1}
list = [123, 321, 34, "hello"]
keywords = [desc: :e_fame], # [{:desc, :e_fame}]

Pattern matching

[head | tail] = [1, 2, 3]
assert head == 1
assert tail == [2, 3]
%{title: some_title} = %{title: "super", description: "blah blah blah"}
assert some_title == "super"

More than just variables

defmodule HumanTalks.Host do
  def thank_you(message) do
    case message do
      %{type: :mail, title: title, sender: sender} ->
        # send automatic mail
      %{type: :sms, content: content, sender: sender} ->
        # automatic Text
      other_message ->
        IO.puts "hey we received an unknown message: #{inspect(other_type)}"
    end
  end
end

Ecto

Disclaimer: this is not a tutorial

Define a model

defmodule HumanTalks.Host do
  use Ecto.Schema

  schema "hosts" do
    field :name     # Defaults to type :string
    field :email
    field :encrypted_password_super_secure # md5 hash
    field :e_fame, :integer, default: -5
    field :last_response, Ecto.DateTime
  end
end

Query the database

defmodule HumanTalks.WhosWho do
  import Ecto.Query, only: [from: 2]
  alias HumanTalks.Host
  alias HumanTalks.Repo

  def most_famous_host do
    query = from(h in Host,
         where: not is_nil(h.e_fame),
         order_by: [desc: :e_fame],
         select: {h.name, h.email},
         limit: 1)
    Repo.one(query)
  end
end

Validate and contrain changes

defmodule HumanTalks.WelcomeHost do
  import Ecto.Changeset
  alias HumanTalks.Host

  def new_host(params \\ %{}) do
    %Host{}
    |> cast(params, [:name, :email, :e_fame])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:e_fame, :greater_than_or_equal_to, 0)
    |> unique_constraint(:email)
  end
end

Insert changeset

defmodule HumanTalks.Reception do
  alias HumanTalks.WelcomeHost

  def welcome_new_host(params) do
    insert_changes = WelcomeHost.new_host(params)
    # insert_changes.is_valid? == true
    case Repo.insert(insert_changes) do
      {:ok, host} ->
        # host.name == params["name"]
      {:error, changeset} ->
        IO.inspect changeset.errors
        #=> [email: {"has already been taken", []}]
    end
  end
end

And many more!

  • Easy associations
  • Smart transactions
  • Rails-like migrations

Sum up

  • Testable as hell
  • No hidden magic
  • Based on an awesome language

Working with associations

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field :name, :string
    field :age, :integer, default: 0
    has_many :comments, Comment
  end
end

Join tables

# Create a query
query = from p in Post,
          join: c in Comment, where: c.post_id == p.id

# Extend the query
query = from [p, c] in query,
          select: {p.title, c.body}

# Or
query = from p in Post, preload: [:comments]

Transaction with Multi

defmodule HumanTalks.PasswordManager do
  alias Ecto.Multi

  def reset(account, params) do
    Multi.new
    |> Multi.update(:account, Account.password_reset_changeset(account, params))
    |> Multi.insert(:log, Log.password_reset_changeset(account, params))
    |> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions))
  end

  def perform(multi) do
    HumanTalks.Repo.transaction(multi)
  end
end

Test multi transaction

test "dry run password reset" do
  account = %HumanTalks.Host{password: "letmein"}
  multi = HumanTalks.PasswordManager.reset(account, params)

  assert [
    {:account, {:update, account_changeset, []}},
    {:log, {:insert, log_changeset, []}},
    {:sessions, {:delete_all, query, []}}
  ] = Ecto.Multi.to_list(multi)

  # We can introspect changesets and query to see if everything
  assert account_changeset.valid?
  assert log_changeset.valid?
  assert inspect(query) == "#Ecto.Query<from a in Session>"
end

Mutability with GenServer

defmofule Stack do
  use GenServer

  def handle_call(:pop, _from, [h | t]) do
    {:reply, h, t}
  end

  def handle_cast({:push, item}, state) do
    {:noreply, [item | state]}
  end
end

{:ok, pid} = GenServer.start_link(Stack, [:hello])
GenServer.call(pid, :pop) #=> :hello
GenServer.cast(pid, {:push, :world}) #=> :ok
GenServer.call(pid, :pop) #=> :world