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/1`:
- `all/2`
- `get/3`
- `get!/3`
- `get_by/3`
- `get_by!/3`
- `one/2`
- `one!/2`
Hooks to `MyApp.EctoSchema.after_delete/1`:
- `delete/2`
- `delete!/2`
Hooks to `MyApp.EctoSchema.after_insert/1`:
- `insert/2`
- `insert!/2`
Hooks to `MyApp.EctoSchema.after_update/1`:
- `update/2`
- `update!/2`
Hooks to `MyApp.EctoSchema.after_insert/1` or to `MyApp.Ecto.Schema.after_update/1`:
- `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.
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. Note that because any business logic is executed synchronously after the
hooked `Ecto.Repo` callback, one should avoid doing any blocking or potentially
terminating logic within hooks as weird or strange behaviour may occur.
## 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)}
end
end
def insert!(changeset, opts) do
changeset
|> @hooks.before_insert
|> super(opts)
|> @hooks.after_insert
end
def update(changeset, opts) do
changeset = @hooks.before_update(changeset)
with {:ok, result} <- super(changeset, opts) do
{:ok, @hooks.after_update(result)}
end
end
def update!(changeset, opts) do
changeset
|> @hooks.before_update
|> super(opts)
|> @hooks.after_update
end
def get(query, id, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, id, opts) do
@hooks.after_get(result)
end
end
def get!(query, id, opts) do
query
|> super(id, opts)
|> @hooks.after_get
end
def get_by(query, clauses, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, clauses, opts) do
@hooks.after_get(result)
end
end
def get_by!(query, clauses, opts) do
query
|> super(clauses, opts)
|> @hooks.after_get
end
def one(query, opts) do
with %{__meta__: %Ecto.Schema.Metadata{}} = result <- super(query, opts) do
@hooks.after_get(result)
end
end
def one!(query, opts) do
query
|> super(opts)
|> @hooks.after_get
end
def all(query, opts) do
query
|> super(opts)
|> Enum.map(&@hooks.after_get/1)
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)}
end
end
def delete!(changeset_or_query, opts) do
changeset_or_query
|> @hooks.before_delete
|> super(opts)
|> @hooks.after_delete
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)}
end
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)}
end
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
|> super(opts)
|> @hooks.after_update
end
def insert_or_update!(
%Ecto.Changeset{data: %{__meta__: %{state: :built}}} = changeset,
opts
) do
changeset
|> @hooks.before_insert
|> super(opts)
|> @hooks.after_insert
end
def insert_or_update!(changeset, opts) do
super(changeset, opts)
end
end
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 function_exported?(schema, unquote(callback), 1) do
schema.unquote(callback)(changeset)
else
changeset
end
end
def unquote(callback)(%schema{} = data) do
if function_exported?(schema, unquote(callback), 1) do
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) do
if function_exported?(schema, unquote(callback), 1) do
schema.unquote(callback)(data)
else
data
end
end
def unquote(callback)(data) do
data
end
end
end