Packages
Adds callbacks/hooks to Ecto: `after_insert`, `after_update`, `after_delete`, `after_get`, `before_insert`, `before_update`, `before_delete`. Useful for setting virtual fields and centralising logic.
Current section
Files
Jump to
Current section
Files
lib/ecto_hooks.ex
defmodule EctoHooks do
@moduledoc """
When `use`-ed in a module that also `use`-es `Ecto.Repo`, augments the following
`Ecto.Repo` callbacks to provide user definable hooks following successful
execution.
Hooks to `MyApp.EctoSchema.after_get/2`:
- `all/2`
- `get/3`
- `get!/3`
- `get_by/3`
- `get_by!/3`
- `one/2`
- `one!/2`
Hooks to `MyApp.EctoSchema.after_delete/2`:
- `delete/2`
- `delete!/2`
Hooks to `MyApp.EctoSchema.after_insert/2`:
- `insert/2`
- `insert!/2`
Hooks to `MyApp.EctoSchema.after_update/2`:
- `update/2`
- `update!/2`
Hooks to `MyApp.EctoSchema.after_insert/2` or to `MyApp.Ecto.Schema.after_update/2`:
- `insert_or_update/2`
- `insert_or_update!/2`
Hooks to `MyApp.EctoSchema.before_delete/1`:
- `delete/2`
- `delete!/2`
Hooks to `MyApp.EctoSchema.before_insert/1`:
- `insert/2`
- `insert!/2`
Hooks to `MyApp.EctoSchema.before_update/1`:
- `update/2`
- `update!/2`
Hooks to `MyApp.EctoSchema.before_insert/1` or to `MyApp.Ecto.Schema.before_update/1`:
- `insert_or_update/2`
- `insert_or_update!/2`
Please note that for all `after_*` hooks, the result of executing a `MyApp.Repo.*` callback
is what ultimately gets returned from the hook, and thus you should aim to write logic
that is transparent and does not break the expected semantics or behaviour of said
callback.
Also, all `after_*` hooks are provided with the changeset, query, or schema used for
insertion as a second parameter, which in some cases can be helpful for intuiting
the diff between the result before versus after running a repo operation.
Any results wrapped within an `{:ok, _}` or `{:error, _}` are also returned re-wrapped
as expected.
For all `before_*` hooks, the result returned by hook is passed directly to the `MyApp.Repo.*`
callback called and thus care must be made to be aware of any implicit changes to changesets
prior to writing to the database.
The hooking functionality provided by `EctoHooks` can be pretty useful for resolving
virtual fields, but can also prove useful for centralising some logging or telemetry
logic.
All defined hooks are executed synchronously immediately before or after calling into
your configured database. To prevent the potential for hooks to infinite loop before
returning, by default, EctoHooks will not trigger more than once within a single
Repo call. You can opt out of this via calling `EctoHooks.enable_hooks/0` in any
of your defined hooks.
This infinite loop protection only currently works within a given process. Take care
when a defined hook may spawn other processes which may trigger database updates which
themselves result in hooks being called.
## Example usage:
```elixir
def MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
use EctoHooks
end
def MyApp.User do
use Ecto.Changeset
require Logger
schema "users" do
field :first_name, :string
field :last_name, :string
field :full_name, :string, virtual: true
end
def before_insert(changeset) do
Logger.warning("updating a user...")
changeset
end
def after_get(%__MODULE__{first_name: first_name, last_name: last_name} = user) do
%__MODULE__{user | full_name: first_name <> " " <> last_name}
end
end
```
"""
defmacro __using__(_opts) do
quote do
@hooks unquote(__MODULE__)
defoverridable all: 2,
delete: 2,
delete!: 2,
get: 3,
get!: 3,
get_by: 3,
get_by!: 3,
insert: 2,
insert!: 2,
insert_or_update: 2,
insert_or_update!: 2,
one: 2,
one!: 2,
update: 2,
update!: 2
def insert(changeset, opts) do
changeset = @hooks.before_insert(changeset)
with {:ok, result} <- super(changeset, opts) do
{:ok, @hooks.after_insert(result, changeset)}
end
after
@hooks.enable_hooks()
end
def insert!(changeset, opts) do
changeset = @hooks.before_insert(changeset)
changeset
|> super(opts)
|> @hooks.after_insert(changeset)
after
@hooks.enable_hooks()
end
def update(changeset, opts) do
changeset = @hooks.before_update(changeset)
with {:ok, result} <- super(changeset, opts) do
{:ok, @hooks.after_update(result, changeset)}
end
after
@hooks.enable_hooks()
end
def update!(changeset, opts) do
changeset = @hooks.before_update(changeset)
changeset
|> super(opts)
|> @hooks.after_update(changeset)
after
@hooks.enable_hooks()
end
def get(query, id, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, id, opts) do
@hooks.after_get(result, query)
end
after
@hooks.enable_hooks()
end
def get!(query, id, opts) do
query
|> super(id, opts)
|> @hooks.after_get(query)
after
@hooks.enable_hooks()
end
def get_by(query, clauses, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, clauses, opts) do
@hooks.after_get(result, query)
end
after
@hooks.enable_hooks()
end
def get_by!(query, clauses, opts) do
query
|> super(clauses, opts)
|> @hooks.after_get(query)
after
@hooks.enable_hooks()
end
def one(query, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, opts) do
@hooks.after_get(result, query)
end
after
@hooks.enable_hooks()
end
def one!(query, opts) do
query
|> super(opts)
|> @hooks.after_get(query)
after
@hooks.enable_hooks()
end
def all(query, opts) do
query
|> super(opts)
|> Enum.map(&@hooks.after_get(&1, query))
after
@hooks.enable_hooks()
end
def delete(changeset_or_query, opts) do
changeset_or_query = @hooks.before_delete(changeset_or_query)
with {:ok, result} <- super(changeset_or_query, opts) do
{:ok, @hooks.after_delete(result, changeset_or_query)}
end
after
@hooks.enable_hooks()
end
def delete!(changeset_or_query, opts) do
changeset_or_query = @hooks.before_delete(changeset_or_query)
changeset_or_query
|> super(opts)
|> @hooks.after_delete(changeset_or_query)
after
@hooks.enable_hooks()
end
def insert_or_update(
%Ecto.Changeset{data: %{__meta__: %{state: :loaded}}} = changeset,
opts
) do
changeset = @hooks.before_update(changeset)
with {:ok, result} <- super(changeset, opts) do
{:ok, @hooks.after_update(result, changeset)}
end
after
@hooks.enable_hooks()
end
def insert_or_update(
%Ecto.Changeset{data: %{__meta__: %{state: :built}}} = changeset,
opts
) do
changeset = @hooks.before_insert(changeset)
with {:ok, result} <- super(changeset, opts) do
{:ok, @hooks.after_insert(result, changeset)}
end
after
@hooks.enable_hooks()
end
def insert_or_update(changeset, opts) do
super(changeset, opts)
end
def insert_or_update!(
%Ecto.Changeset{data: %{__meta__: %{state: :loaded}}} = changeset,
opts
) do
changeset = @hooks.before_update(changeset)
changeset
|> super(opts)
|> @hooks.after_update(changeset)
after
@hooks.enable_hooks()
end
def insert_or_update!(
%Ecto.Changeset{data: %{__meta__: %{state: :built}}} = changeset,
opts
) do
changeset = @hooks.before_insert(changeset)
changeset
|> super(opts)
|> @hooks.after_insert(changeset)
after
@hooks.enable_hooks()
end
def insert_or_update!(changeset, opts) do
super(changeset, opts)
end
end
end
@doc """
Disables EctoHooks from running for all future Repo operations in the current process.
"""
def disable_hooks do
Process.put({__MODULE__, :hooks_enabled}, false)
:ok
end
@doc """
Enables EctoHooks from running for all future Repo operations in the current process.
"""
def enable_hooks do
Process.put({__MODULE__, :hooks_enabled}, true)
:ok
end
@doc """
Returns a boolean indicating if EctoHooks are enabled in the current process.
"""
def hooks_enabled? do
Process.get({__MODULE__, :hooks_enabled}, true)
end
@before_callbacks [:before_delete, :before_insert, :before_update]
@after_callbacks [:after_delete, :after_get, :after_insert, :after_update]
for callback <- @before_callbacks do
@doc false
def unquote(callback)(%{__struct__: Ecto.Changeset, data: %schema{}} = changeset) do
if hooks_enabled?() && function_exported?(schema, unquote(callback), 1) do
disable_hooks()
schema.unquote(callback)(changeset)
else
changeset
end
end
def unquote(callback)(%schema{} = data) do
if hooks_enabled?() && function_exported?(schema, unquote(callback), 1) do
disable_hooks()
schema.unquote(callback)(data)
else
data
end
end
def unquote(callback)(changeset) do
changeset
end
end
for callback <- @after_callbacks do
@doc false
def unquote(callback)(%schema{} = data, changeset_query_or_schema) do
if hooks_enabled?() && function_exported?(schema, unquote(callback), 2) do
disable_hooks()
schema.unquote(callback)(data, changeset_query_or_schema)
else
data
end
end
def unquote(callback)(data, _changeset_query_or_schema) do
data
end
end
end