Recursive Relations
Overview
Section titled “Overview”A common pattern in domain models is nested parent/child relationships, i.e. a parent (a manager Employee) that has multiple children (their direct reports Employees), which themselves can have multiple children (their own direct report Employees).
These relationships are modeled by self-referential FKs or m2m tables, i.e.:
- A
employees.manager_idFK for the manager/reports example, or - A
task_to_task_dependenciesm2m table that tracks a task having other tasks as dependencies
When Joist sees self-referential relations, it automatically creates both the “immediate” relations, and “recursive” relations that will fetch the whole tree of parents/children in a single SQL call:
class Employee { // standard "immediate" relations manager: Reference<Employee, Employee>; reports: Collection<Employee, Employee[]>; // additional "recursive" relations managersRecursive: Reference<Employee, Employee>; reportsRecursive: Collection<Employee, Employee[]>;}Such that we can use reportsRecursive to fetch all of a manager’s reports, and all their reports, etc. in a single method call and single SQL query:
await m1.reportsRecursive.load();Joist uses Postgres’s recursive CTE support to implement the recursive relations, so the above code will result in a single SQL query that fetches all of m1’s reports, and all their reports, etc.
Consistent View
Section titled “Consistent View”As with all Joist relations, recursive relations provide a “consistent view” of the entity graph that is always in sync with any WIP/un-flushed mutations you’ve made.
For example, if you’ve modified the employee/manager relationship for any employees in the current EntityManager, and then later call either managersRecursive or reportsRecursive, we will load the recursive data from the database (if not already loaded), and also apply any WIP, uncommitted changes to the hierarchy.
This ensures your code can rely on the recursive relations to be up-to-date, and should dramatically simplify reasoning about/enforcing rules while persisting changes.
Cycle Detection
Section titled “Cycle Detection”Recursive relations always fail (throw a RecursiveCycleError exception) when they detect cycles during .get calls.
We do not automatically add validation rules to enforce no cycles, but you can opt-in to cycle detection during validation by using addCycleRule:
config.addCycleRule( "reportsRecursive", (e) => `Manager ${e.name} has a cycle in their direct reports`,);Disabling Recursive Relations
Section titled “Disabling Recursive Relations”If you don’t want/need the recursive relations, you can disable them by setting skipRecursiveRelations: true in joist-config.json for the self-referencing m2o relation, i.e.:
{ "entities": { "User": { "tag": "u", "relations": { "manager": { "skipRecursiveRelations": true } } } }}