Skip to content

Instantly share code, notes, and snippets.

@imranismail
Last active July 28, 2017 08:48
Show Gist options
  • Save imranismail/810669b0a30fc59efc45fadf75a8201c to your computer and use it in GitHub Desktop.
Save imranismail/810669b0a30fc59efc45fadf75a8201c to your computer and use it in GitHub Desktop.
Repo composition

Repo Composition

Motive

Ecto.Repo by default doesn't allow overrides. So when you need to compose functions but maintain an API parity with the Ecto.Repo. You'd need to compose the functions in another module and things can get messy really fast that way.

Can't invoke super

== Compilation error on file lib/app/repo.ex ==
** (CompileError) lib/app/repo.ex:6: no super defined for all/2 in module App.Repo. Overridable functions available are:
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (elixir) lib/kernel/parallel_compiler.ex:117: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1

Sure you can solve this using service modules, but sometimes you might want some sort of functionality for every database operations.

With this you can easily compose some of the Repo functions but maintain an API parity without writing all the delegation functions 😎

How?

With this utility module

defmodule ComposableRepo do
  defmacro __using__(opts) do
    quote bind_quoted: binding() do
      @composable_repo Keyword.fetch!(opts, :composing)
      @composable_functions @composable_repo.__info__(:functions)

      @composable_functions
      |> Enum.map(fn
        {function, 0} ->
          {function, []}
        {function, arity} ->
          {function, Enum.map(1..arity, &(Macro.var(:"arg_#{&1}", __MODULE__)))}
      end)
      |> Enum.map(fn {function, arguments} ->
        defdelegate unquote(function)(unquote_splicing(arguments)), to: @composable_repo
      end)

      defoverridable @composable_functions
    end
  end
end

Usage

defmodule App.OriginalRepo do
  use Ecto.Repo, otp_app: :my_app
end

defmodule App.ComposedRepo do
  use ComposableRepo, composing: App.OriginalRepo
  
  # Now you can override the repo functions to 
  # add maybe an event bus for schema inserts
  def insert(struct_or_changeset, opts \\ []) do
    transaction(fn ->
      case super(struct_or_changeset, opts) do
        {:ok, schema} ->
          :ok = App.Endpoint.broadcast(:schema_inserts, schema.__struct__, schema)
          schema
        {:error, changeset} -> 
          rollback(changeset)
      end
    end)
  end
end

Might wanna test it out on IEx

iex> App.Endpoint.subscribe(:schema_inserts)
iex> {:ok, user} = App.ComposedRepo.insert(%User{name: "imran"})
iex> user == App.ComposedRepo.one(User)
iex> flush()
@imranismail
Copy link
Author

With the Elixir 1.5, you can now disregard this and use the following instead

defmodule App.Repo do
  use Ecto.Repo, otp_app: :app

  defoverridable Ecto.Repo

  def insert(struct_or_changeset) do
    transaction(fn ->
      case super(struct_or_changeset) do
        {:ok, schema} ->
          :ok = App.Endpoint.broadcast(:schema_inserts, schema.__struct__, schema)
          schema
        {:error, changeset} -> 
          rollback(changeset)
      end
    end)
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment