Skip to content

Instantly share code, notes, and snippets.

@imranismail
Forked from ahmadshah/README.md
Created August 25, 2016 18:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save imranismail/2cda435f99e029c119cb26b9e560c613 to your computer and use it in GitHub Desktop.
Save imranismail/2cda435f99e029c119cb26b9e560c613 to your computer and use it in GitHub Desktop.
Ecto Soft Delete

Soft Delete Ecto Repo

The goal is to support soft delete functionality in Ecto.Repo. With the suggestion by @imranismail, another repo is created and the remaining functionalities are delegate to the original MyApp.Repo.

The new repo get/2 and all/1 functions will exclude the soft deleted record by default. delete/1 and delete_all/1 will update the delete_at column by default instead of deleting.

Example

MyApp.Repo.get(MyApp.User, 1) //will return nil if record is in soft delete state

MyApp.Repo.get(MyApp.User, 1, [with_thrash: true]) //will return the soft deleted record

MyApp.Repo.all(MyApp.User) //will exclude soft deleted records

MyApp.Repo.all(MyApp.User, [with_thrash: true]) //will include soft deleted records

MyApp.Repo.delete(user) //will update the deleted_at column

MyApp.Repo.delete(user, [force: true]) //will permanently delete the record

MyApp.Repo.delete_all(MyApp.User) //will updated the deleted_at columns

MyApp.Repo.delete_all(MyApp.User, [force: true]) //will permanently delete all records

MyApp.Repo.restore(MyApp.User, 1) //will restore back the soft deleted record
defmodule MyApp.CustomRepo do
@behaviour MyApp.SoftDelete
import Ecto.Query, only: [where: 2]
import Ecto.DateTime, only: [utc: 0]
import Ecto.Changeset, only: [change: 2]
import Ecto.Queryable, only: [to_query: 1]
alias MyApp.Repo
require Ecto.Query
def all(queryable, opts \\ [])
def all(queryable, opts) when length(opts) == 0 do
queryable = exclude_thrash(queryable)
Repo.all(queryable)
end
def all(queryable, opts) when length(opts) > 0 do
case with_thrash_option?(opts) do
true ->
queryable = exclude_thrash(queryable, false)
opts = Keyword.drop(opts, [:with_thrash])
Repo.all(queryable, opts)
_ ->
opts = Keyword.drop(opts, [:with_thrash])
all(queryable, opts)
end
end
def get(queryable, id, opts \\ [])
def get(queryable, id, opts) when length(opts) == 0 do
queryable = exclude_thrash(queryable)
Repo.get(queryable, id, opts)
end
def get(queryable, id, opts) when length(opts) > 0 do
case with_thrash_option?(opts) do
true ->
queryable = exclude_thrash(queryable, false)
opts = Keyword.drop(opts, [:with_thrash])
Repo.get(queryable, id, opts)
_ ->
opts = Keyword.drop(opts, [:with_thrash])
get(queryable, id, opts)
end
end
def delete(struct, opts \\ [])
def delete(struct, opts) when length(opts) == 0 do
changeset = change(struct, deleted_at: utc())
Repo.update(changeset)
end
def delete(struct, opts) when length(opts) > 0 do
case with_force_option?(opts) do
true ->
opts = Keyword.drop(opts, [:force])
Repo.delete(struct, opts)
_ -> delete(struct)
end
end
def delete_all(queryable, opts \\ [])
def delete_all(queryable, opts) when length(opts) == 0 do
Repo.update_all(queryable, set: [deleted_at: utc()])
end
def delete_all(queryable, opts) when length(opts) > 0 do
case with_force_option?(opts) do
true ->
opts = Keyword.drop(opts, [:force])
Repo.delete_all(queryable, opts)
_ ->
delete_all(queryable)
end
end
def restore(queryable, id) do
changeset = change(get!(queryable, id), deleted_at: nil)
update(changeset)
end
defp schema_fields(%{from: {_source, schema}}) when schema != nil, do: schema.__schema__(:fields)
defp field_exists?(queryable, column) do
query = to_query(queryable)
fields = schema_fields(query)
Enum.member?(fields, column)
end
defp exclude_thrash(queryable, exclude \\ true) do
case field_exists?(queryable, :deleted_at) do
false -> queryable
true ->
cond do
exclude -> where(queryable, fragment("deleted_at IS NULL"))
!exclude-> queryable
end
end
end
defp with_thrash_option?(opts), do: Keyword.get(opts, :with_thrash)
defp with_force_option?(opts), do: Keyword.get(opts, :force)
defdelegate config(), to: MyApp.Repo
defdelegate get!(queryable, id, opts \\ []), to: MyApp.Repo
defdelegate get_by(queryable, clauses, opts \\ []), to: MyApp.Repo
defdelegate get_by!(queryable, clauses, opts \\ []), to: MyApp.Repo
defdelegate in_transaction?(), to: MyApp.Repo
defdelegate insert(struct, opts \\ []), to: MyApp.Repo
defdelegate insert!(struct, opts \\ []), to: MyApp.Repo
defdelegate insert_all(schema_or_source, entries, opts \\ []), to: MyApp.Repo
defdelegate insert_or_update(changeset, opts \\ []), to: MyApp.Repo
defdelegate insert_or_update!(changeset, opts \\ []), to: MyApp.Repo
defdelegate one(queryable, opts \\ []), to: MyApp.Repo
defdelegate one!(queryable, opts \\ []), to: MyApp.Repo
defdelegate preload(struct_or_structs, preloads, opts \\ []), to: MyApp.Repo
defdelegate rollback(value), to: MyApp.Repo
defdelegate start_link(opts \\ []), to: MyApp.Repo
defdelegate stop(pid, timeout \\ 5000), to: MyApp.Repo
defdelegate transaction(fun_or_multi, opts \\ []), to: MyApp.Repo
defdelegate update(struct, opts \\ []), to: MyApp.Repo
defdelegate update!(struct, opts \\ []), to: MyApp.Repo
defdelegate update_all(queryable, updates, opts \\ []), to: MyApp.Repo
defdelegate delete!(struct, opts \\ []), to: MyApp.Repo
end
defmodule MyApp.SoftDelete do
@callback restore(Ecto.Queryable.t, integer) :: {:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t}
end
defmodule User do
use Ecto.Schema
schema "users" do
field :email, :string
field :password, :string
field :deleted_at, Ecto.DateTime, null: true
timestamps
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment