Skip to main content

Model and read relationships

Relationships connect focused tables without duplicating their data. TaruviBase derives relationship metadata from the schema's foreign keys.

The patterns below use flat tables. Whole-table JSONB storage has a separate relationship compatibility boundary.

ShapeExampleModel
Belongs toA task belongs to one projecttasks.project_id references projects.id
Has manyA project has many tasksReverse view of tasks.project_id
Many to manyA task can have many tagsA junction table references both tables

Define a belongs-to relationship​

Create the referenced table first. Then add the foreign-key field and foreignKeys definition to the table that owns the reference. For example, add project_id to tasks and reference projects.id.

Use the logical table name in reference.resource. NO ACTION keeps deletion explicit: remove or redirect referencing tasks before deleting their project. Use the foreign-key declaration in the schema reference for the exact descriptor and supported delete actions.

Model many-to-many data​

Create a junction table when both sides can have many records. For tasks and tags, define task_tags with:

  • its own stable id;
  • task_id referencing tasks.id; and
  • tag_id referencing tags.id.

Create, query, and delete junction records through the ordinary record workflows. This makes each relationship explicit and gives it room for fields such as created_at or assigned_by.

Add populate to a list or read to return related records with each result. Use the foreign-key field for a belongs-to relationship, such as populate=project_id on tasks, or the related table's name for the reverse direction, such as populate=tasks on projects. populate=* includes every first-level relationship. In the SDKs, use .populate(['project_id']) in JavaScript or .populate("project_id") in Python.

TaruviBase checks that the caller can read each related table. populate can't be combined with aggregation queries.

To page through related records on their own, or to combine data your own way, read each table in its own request:

  1. Read the root records from their table.
  2. Collect the foreign-key values needed by the application.
  3. Query the related table with the same site, app, and caller context.
  4. Join the records in application code.

This keeps response size, pagination, and authorization visible at each step.

For example, after reading tasks and collecting their project_id values into projectIds, fetch the related projects with one bounded query:

await database
.from('projects')
.filters('id', 'in', projectIds)
.sort('name', 'asc')
.sort('id', 'asc')
.page(1)
.pageSize(20)
.execute();

Build a map keyed by project ID, then attach each project to its task in application code. Apply the same pattern in the other direction by filtering tasks.project_id with the selected project IDs.

Choose a query pattern​

RequirementPattern
Read a foreign-key valueRead the root record normally
Read the referenced recordQuery the referenced table by ID
Read a has-many collectionFilter the child table by its foreign-key field
Read a many-to-many collectionQuery the junction table, then query the target IDs
Filter or sort by related dataQuery the related IDs first, then filter and sort the root table
Show related rows with root rowsUse populate, or read both tables and join them in application code

Separate table reads give you independent pagination and response size for each table.

Relationship checklist​

  • Create the referenced table before the referencing table.
  • Use compatible field types on both sides.
  • Choose delete behavior deliberately.
  • Test valid and missing references.
  • Apply pagination and stable ordering to each collection read.
  • Use populate for small related sets, and separate paged reads for large ones.

Next, use aggregations to summarize the records returned by a table query.