Skip to main content

Recursive Relations

Overview

A common pattern in domain modeling is nested parent/child relationships, i.e. a parent that has multiple children, which themselves can have multiple grand children.

For example an employees.manager_id FK that generates normal many-to-one and one-to-many relations:

class Employee {
manager: Reference<Employee, Employee>;
reports: Collection<Employee, Employee[]>;
}

These relations fetch "the immediate manager" and "the immediate reports", but often you want to fetch "the whole tree", i.e. a manager and all of their reports recursively.

This has historically been difficult in relational databases (requiring approaches like lpath and closure tables, see this blog post), but now can be done in Postgres using recursive CTEs.

Recursive Relations

Whenever Joist sees a foreign key that is self-referential, Joist will automatically create recursive relations, in addition to the "immediate" o2m and m2o, i.e. for a employees.manager_id FK, you'll get:

class Employee {
// immediate relations
manager: Reference<Employee, Employee>;
reports: Collection<Employee, Employee[]>;
// recursive relations
managersRecursive: Reference<Employee, Employee>;
reportsRecursive: Collection<Employee, Employee[]>;
}

And if you call:

await m1.reportsRecursive.load();

Joist will issue a single SQL call to fetch all m1's reports, and all their reports, etc. in a single query.

This method is also automatically batched, so if you invoke it in a loop, or a validation rule, or other business logic, for multiple managers at once, it will still create a single SQL call.

Consistent View

As with other Joist relations, the recursive relations provide a "consistent view" of the entity graph.

For example, if you've modified the employee/manager relationship for any employees in the current EntityManager unit of work, 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

Joist's recursive relations currently always reject cycles.

Note that we do not automatically add an "enforce no cycles" validation rule to recursive relations by default, but if you do access any relation (either recursive parents or recursive children) that does have a cycle, we'll throw an error.

Given this, it's advised to add your own cycle-preventing validation rules.

This behavior should likely be configurable (i.e. to allow cycles), but has not been implemented yet.

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
}
}
}
}
}