Changeset functions that keep the tree consistent.
Ecto has no model callbacks. Instead, these functions register
Ecto.Changeset.prepare_changes/2 callbacks, which the repo runs inside the
same transaction as the insert, update or delete. They take care of:
- resolving
parent_idinto the ancestry column (and rejecting cycles), - rewriting the ancestry of all descendants when a node moves (a single
UPDATE), - keeping the depth and counter caches up to date,
- touching ancestors (
touch: true), - applying the orphan strategy when a node is deleted.
Saving with Repo.insert/2, Repo.update/2 or Repo.delete/2 without going
through these functions bypasses all of the above.
Example
def changeset(category, attrs) do
category
|> cast(attrs, [:name])
|> AncestryEx.Changeset.cast_parent()
end
# create a child, move it, delete its parent
{:ok, child} = Repo.insert(Category.changeset(%Category{}, %{name: "B", parent_id: a.id}))
{:ok, child} = child |> AncestryEx.Changeset.put_parent(other) |> Repo.update()
{:ok, _} = a |> AncestryEx.Changeset.handle_orphans() |> Repo.delete()
Summary
Functions
Casts the parent_id parameter into the ancestry column.
Prepares a node to be deleted with Repo.delete/2, applying the orphan
strategy to its descendants inside the delete transaction.
Sets the parent of the node to parent (a persisted node of the same
schema) or makes it a root when parent is nil.
Functions
@spec cast_parent(Ecto.Changeset.t()) :: Ecto.Changeset.t()
Casts the parent_id parameter into the ancestry column.
Reads "parent_id" (or :parent_id) from the changeset params, or from the
changes if :parent_id was already cast. A nil or empty value makes the
node a root. Otherwise the parent is loaded when the changeset is saved, and
the changeset fails with an error on :parent_id if it does not exist or if
it is the node itself or one of its descendants.
It also validates and tracks any change made directly to the ancestry column, so it should be called in every changeset of the schema.
@spec handle_orphans(Ecto.Changeset.t() | struct(), keyword()) :: Ecto.Changeset.t()
Prepares a node to be deleted with Repo.delete/2, applying the orphan
strategy to its descendants inside the delete transaction.
The strategy defaults to the :orphan_strategy option of the schema and can
be overridden with orphan_strategy: strategy. With :restrict, deleting a
node that has children returns {:error, changeset} with an error on the
ancestry column.
Accepts a changeset or a struct.
category
|> AncestryEx.Changeset.handle_orphans(orphan_strategy: :adopt)
|> Repo.delete()
@spec put_parent(Ecto.Changeset.t() | struct(), struct() | nil) :: Ecto.Changeset.t()
Sets the parent of the node to parent (a persisted node of the same
schema) or makes it a root when parent is nil.
Accepts a changeset or a struct.