# Manual Relationships
See [Manual Relationships](https://hexdocs.pm/ash/relationships.html#manual-relationships) for an idea of manual relationships in general.
Manual relationships allow for expressing complex/non-typical relationships between resources in a standard way.
Individual data layers may interact with manual relationships in their own way, so see their corresponding guides.
## Example
```elixir
# in the resource
relationships do
has_many :tickets_above_threshold, Helpdesk.Support.Ticket do
manual Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold
end
end
# implementation
defmodule Helpdesk.Support.Ticket.Relationships.TicketsAboveThreshold do
use Ash.Resource.ManualRelationship
use AshPostgres.ManualRelationship
require Ash.Query
require Ecto.Query
def load(records, _opts, %{query: query, actor: actor, authorize?: authorize?}) do
# Use existing records to limit resultds
rep_ids = Enum.map(records, & &1.id)
# Using Ash to get the destination records is ideal, so you can authorize access like normal
# but if you need to use a raw ecto query here, you can. As long as you return the right structure.
{:ok,
query
|> Ash.Query.filter(representative_id in ^rep_ids)
|> Ash.Query.filter(priority > representative.priority_threshold)
|> Helpdesk.Support.read!(actor: actor, authorize?: authorize?)
# Return the items grouped by the primary key of the source, i.e representative.id => [...tickets above threshold]
|> Enum.group_by(& &1.representative_id)}
end
# query is the "source" query that is being built.
# _opts are options provided to the manual relationship, i.e `{Manual, opt: :val}`
# current_binding is what the source of the relationship is bound to. Access fields with `as(^current_binding).field`
# as_binding is the binding that your join should create. When you join, make sure you say `as: ^as_binding` on the
# part of the query that represents the destination of the relationship
# type is `:inner` or `:left`.
# destination_query is what you should join to to add the destination to the query, i.e `join: dest in ^destination-query`
def ash_postgres_join(query, _opts, current_binding, as_binding, :inner, destination_query) do
{:ok,
Ecto.Query.from(_ in query,
join: dest in ^destination_query,
as: ^as_binding,
on: dest.representative_id == as(^current_binding).id,
on: dest.priority > as(^current_binding).priority_threshold
)}
end
def ash_postgres_join(query, _opts, current_binding, as_binding, :left, destination_query) do
{:ok,
Ecto.Query.from(_ in query,
left_join: dest in ^destination_query,
as: ^as_binding,
on: dest.representative_id == as(^current_binding).id,
on: dest.priority > as(^current_binding).priority_threshold
)}
end
# _opts are options provided to the manual relationship, i.e `{Manual, opt: :val}`
# current_binding is what the source of the relationship is bound to. Access fields with `parent_as(^current_binding).field`
# as_binding is the binding that has already been created for your join. Access fields on it via `as(^as_binding)`
# destination_query is what you should use as the basis of your query
def ash_postgres_subquery(_opts, current_binding, as_binding, destination_query) do
{:ok,
Ecto.Query.from(_ in destination_query,
where: parent_as(^current_binding).id == as(^as_binding).representative_id,
where: as(^as_binding).priority > parent_as(^current_binding).priority_threshold
)}
end
end
```
## Recursive Relationships
Manual relationships can be _very_ powerful, as they can leverage the full power of Ecto to do arbitrarily complex things.
Here is an example of a recursive relationship that loads all employees under the purview of a given manager using a recursive CTE.
> ### Use ltree {: .info}
>
> While the below is very powerful, if at all possible we suggest using ltree for hierarchical data. Its built in to postgres
> and AshPostgres has built in support for it. For more, see: `AshPostgres.Ltree`.
Keep in mind this is an example of a very advanced use case, _not_ something you'd typically need to do.
```elixir
defmodule MyApp.Employee.ManagedEmployees do
@moduledoc """
A manual relationship which uses a recursive CTE to find all employees managed by a given employee.
"""
use Ash.Resource.ManualRelationship
use AshPostgres.ManualRelationship
alias MyApp.Employee
alias MyApp.Repo
import Ecto.Query
@doc false
@impl true
@spec load([Employee.t()], keyword, map) ::
{:ok, %{Ash.UUID.t() => [Employee.t()]}} | {:error, any}
def load(employees, _opts, _context) do
employee_ids = Enum.map(employees, & &1.id)
all_descendants =
Employee
|> where([l], l.manager_id in ^employee_ids)
|> recursive_cte_query("employee_tree", Employee)
|> Repo.all()
employees
|> with_descendants(all_descendants)
|> Map.new(&{&1.id, &1.descendants})
|> then(&{:ok, &1})
end
defp with_descendants([], _), do: []
defp with_descendants(employees, all_descendants) do
Enum.map(employees, fn employee ->
descendants = Map.get(all_descendants, employee.id, [])
%{employee | descendants: with_descendants(descendants, all_descendants)}
end)
end
@doc false
@impl true
@spec ash_postgres_join(
Ecto.Query.t(),
opts :: keyword,
current_binding :: any,
as_binding :: any,
:inner | :left,
Ecto.Query.t()
) ::
{:ok, Ecto.Query.t()} | {:error, any}
# Add a join from some binding in the query, producing *as_binding*.
def ash_postgres_join(query, _opts, current_binding, as_binding, join_type, destination_query) do
immediate_parents =
from(destination in destination_query,
where: parent_as(^current_binding).manager_id == destination.id
)
cte_name = "employees_#{as_binding}"
descendant_query =
recursive_cte_query_for_join(
immediate_parents,
cte_name,
destination_query
)
case join_type do
:inner ->
{:ok,
from(row in query,
inner_lateral_join: descendant in subquery(descendant_query),
on: true,
as: ^as_binding
)}
:left ->
{:ok,
from(row in query,
left_lateral_join: descendant in subquery(descendant_query),
on: true,
as: ^as_binding
)}
end
end
@impl true
@spec ash_postgres_subquery(keyword, any, any, Ecto.Query.t()) ::
{:ok, Ecto.Query.t()} | {:error, any}
# Produce a subquery using which will use the given binding and will be
def ash_postgres_subquery(_opts, current_binding, as_binding, destination_query) do
immediate_descendants =
from(destination in Employee,
where: parent_as(^current_binding).id == destination.manager_id
)
cte_name = "employees_#{as_binding}"
recursive_cte_query =
recursive_cte_query_for_join(
immediate_descendants,
cte_name,
Employee
)
other_query =
from(row in subquery(recursive_cte_query),
where:
row.id in subquery(
from(row in Ecto.Query.exclude(destination_query, :select), select: row.id)
)
)
{:ok, other_query}
end
defp recursive_cte_query(immediate_parents, cte_name, query) do
recursion_query =
query
|> join(:inner, [l], lt in ^cte_name, on: l.manager_id == lt.id)
descendants_query =
immediate_parents
|> union(^recursion_query)
{cte_name, Employee}
|> recursive_ctes(true)
|> with_cte(^cte_name, as: ^descendants_query)
end
defp recursive_cte_query_for_join(immediate_parents, cte_name, query) do
# This is due to limitations in ecto's recursive CTE implementation
# For more, see here:
# https://elixirforum.com/t/ecto-cte-queries-without-a-prefix/33148/2
# https://stackoverflow.com/questions/39458572/ecto-declare-schema-for-a-query
employee_keys = Employee.__schema__(:fields)
cte_name_ref =
from(cte in fragment("?", literal(^cte_name)), select: map(cte, ^employee_keys))
recursion_query =
query
|> join(:inner, [l], lt in ^cte_name_ref, on: l.manager_id == lt.id)
descendants_query =
immediate_parents
|> union(^recursion_query)
cte_name_ref
|> recursive_ctes(true)
|> with_cte(^cte_name, as: ^descendants_query)
end
end
```
With the above definition, employees could have a relationship like this:
```elixir
has_many :managed_employees, MyApp.Employee do
manual MyApp.Employee.ManagedEmployees
end
```
And you could then use it in calculations and aggregates! For example, to see the count of employees managed by each employee:
```elixir
aggregates do
count :count_of_managed_employees, :managed_employees
end
```