Skip to main content
Relations define associations between Entities to ensure database referential integrity and enable type-safe join queries.

Relation Types Overview

Sonamu supports 4 Relation types:

BelongsToOne

N:1 relationship - Many reference one Example: Post → User (multiple posts belong to one user)

OneToOne

1:1 relationship - One references one Example: User ↔ Employee (user and employee info are 1:1 matched)

HasMany

1:N relationship - One owns many Example: User → Posts (one user owns multiple posts)

ManyToMany

N:M relationship - Many-to-many Example: Post ↔ Tag (many-to-many between posts and tags)

BelongsToOne

N:1 relationship - The current Entity belongs to another Entity.

Basic Usage

post.entity.json
{
  "id": "Post",
  "props": [
    {
      "type": "relation",
      "name": "user",
      "with": "User",
      "relationType": "BelongsToOne",
      "desc": "Author"
    }
  ]
}
Generated column: user_id (integer, not null) Database structure:

Options

OptionTypeDescriptionDefault
nullablebooleanAllow NULLfalse
useConstraintbooleanUse Foreign Key constrainttrue
onUpdateRelationOnAction on referenced record updateRESTRICT
onDeleteRelationOnAction on referenced record deleteRESTRICT
customJoinClausestringCustom JOIN condition-

RelationOn Options

ValueDescriptionUse Case
CASCADEChange/delete child when parent changesDelete posts when user is deleted
SET NULLSet child’s FK to NULL when parent deletedSet employee’s department to NULL when department deleted
RESTRICTPrevent parent change/delete if children existCannot delete user if posts exist
NO ACTIONSimilar to RESTRICT, different check timing-
SET DEFAULTSet to default value when parent deleted-

Example: nullable and CASCADE

{
  "type": "relation",
  "name": "department",
  "with": "Department",
  "relationType": "BelongsToOne",
  "nullable": true,
  "onDelete": "SET NULL",
  "desc": "Department"
}
Behavior:
  • department_id allows NULL
  • When department is deleted, employee’s department_id is set to NULL

TypeScript Usage

// Include relation in Subset
const posts = await this.puri()
  .select<PostSubsetA>("A")
  .many();

// Can access posts[0].user.username

OneToOne

1:1 relationship - Two Entities reference each other exactly once.

Basic Usage

OneToOne can be defined in two ways:
FK column is created in the current Entity.
employee.entity.json
{
  "id": "Employee",
  "props": [
    {
      "type": "relation",
      "name": "user",
      "with": "User",
      "relationType": "OneToOne",
      "hasJoinColumn": true,
      "onDelete": "CASCADE",
      "desc": "User account"
    }
  ]
}
Generated column: user_id (integer, unique, not null)Database structure:
users: id, username
employees: id, user_id (FK, UNIQUE), employee_number

Options

OptionTypeDescriptionDefault
hasJoinColumnbooleanWhether to create FK columnrequired
nullablebooleanAllow NULL (when hasJoinColumn: true)false
useConstraintbooleanFK constraint (when hasJoinColumn: true)true
onUpdateRelationOnAction on update (when hasJoinColumn: true)RESTRICT
onDeleteRelationOnAction on delete (when hasJoinColumn: true)RESTRICT
customJoinClausestringCustom JOIN condition-

Example: Bidirectional OneToOne

{
  "type": "relation",
  "name": "employee",
  "with": "Employee",
  "relationType": "OneToOne",
  "hasJoinColumn": false,
  "nullable": true
}
Relationship explained:
  • User can optionally have an Employee (nullable)
  • Employee must have a User (not null)
  • When User is deleted, Employee is also deleted (CASCADE)

HasMany

1:N relationship - One Entity owns multiple other Entities.

Basic Usage

user.entity.json
{
  "id": "User",
  "props": [
    {
      "type": "relation",
      "name": "posts",
      "with": "Post",
      "relationType": "HasMany",
      "joinColumn": "user_id",
      "desc": "Written posts"
    }
  ]
}
Requirements:
  • Post Entity must have a user_id column
  • Usually defined with BelongsToOne in reverse on Post

Options

OptionTypeDescriptionDefault
joinColumnstringFK column name in the other tablerequired
fromColumnstringReference column name in current tableid
nullablebooleanNullable for the relation itself (optional)false

Example: Using fromColumn

{
  "type": "relation",
  "name": "childPosts",
  "with": "Post",
  "relationType": "HasMany",
  "joinColumn": "parent_post_id",
  "fromColumn": "id",
  "desc": "Child posts"
}
JOIN query:
SELECT * FROM posts
WHERE posts.parent_post_id = {user.id}

TypeScript Usage

{
  "subsets": {
    "A": [
      "id",
      "username",
      "posts.id",
      "posts.title",
      "posts.created_at"
    ]
  }
}
HasMany is automatically optimized using the DataLoader pattern. N+1 query problems don’t occur.

ManyToMany

N:M relationship - Many-to-many relationship implemented through a join table.

Basic Usage

post.entity.json
{
  "id": "Post",
  "props": [
    {
      "type": "relation",
      "name": "tags",
      "with": "Tag",
      "relationType": "ManyToMany",
      "joinTable": "posts__tags",
      "onUpdate": "CASCADE",
      "onDelete": "CASCADE",
      "desc": "Tags"
    }
  ]
}
Auto-generated Join Table:
CREATE TABLE posts__tags (
  id INTEGER PRIMARY KEY,
  post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
  UNIQUE(post_id, tag_id)
);

Options

OptionTypeDescriptionDefault
joinTablestringJoin table name ({table1}__${table2})required
onUpdateRelationOnAction on referenced record updaterequired
onDeleteRelationOnAction on referenced record deleterequired
nullablebooleanNullable for the relation itself (optional)false
Join Table Naming Convention: Sort two table names alphabetically and connect with __. - Correct: posts__tags - Wrong: tags__posts (not alphabetical order)

Bidirectional Definition

{
  "type": "relation",
  "name": "tags",
  "with": "Tag",
  "relationType": "ManyToMany",
  "joinTable": "posts__tags",
  "onUpdate": "CASCADE",
  "onDelete": "CASCADE"
}

TypeScript Usage

{
  "subsets": {
    "A": [
      "id",
      "title",
      "tags.id",
      "tags.name"
    ]
  }
}

Custom Join Clause

You can write SQL expressions directly when complex JOIN conditions are needed.
{
  "type": "relation",
  "name": "latestPost",
  "with": "Post",
  "relationType": "OneToOne",
  "hasJoinColumn": false,
  "customJoinClause": "users.id = posts.user_id AND posts.is_published = true",
  "desc": "Latest published post"
}
customJoinClause is an advanced feature. Use standard Relations when possible.

Relation Usage Patterns

1. Selecting Relation Fields in Subsets

{
  "subsets": {
    "A": ["id", "title", "user.username", "user.email", "tags.name"]
  }
}
Auto-generated queries:
  • user: LEFT JOIN
  • tags: Separate query via DataLoader

2. Nested Relations

{
  "subsets": {
    "A": ["id", "title", "user.id", "user.username", "user.employee.department.name"]
  }
}
Sonamu automatically generates the necessary JOINs:
FROM posts
LEFT JOIN users ON posts.user_id = users.id
LEFT JOIN employees ON users.id = employees.user_id
LEFT JOIN departments ON employees.department_id = departments.id

3. Filtering Relations

const posts = await this.puri()
  .where("user.role", "admin")
  .many();

4. Sorting by Relations

const posts = await this.puri().orderBy("user.username", "asc").many();

Relation Design Guide

BelongsToOne vs OneToOne

SituationRecommended TypeReason
Post → AuthorBelongsToOneMultiple posts belong to one user
User ↔ ProfileOneToOne1:1 matching relationship
Order → CustomerBelongsToOneMultiple orders belong to one customer

CASCADE vs RESTRICT

SituationRecommended ActionReason
User delete → PostsCASCADEDelete together
Category delete → PostsRESTRICTCannot delete if posts exist
Department delete → EmployeesSET NULLKeep employees, set department to NULL

nullable Setting

SituationnullableReason
Required relationshipfalseReference always needed
Optional relationshiptrueMay not exist
Temporary statetrueSet later

Cautions

Avoid Circular ReferencesAvoid circular references like A → B → C → A. This can cause problems when creating data.
JOIN Depth LimitToo deep nested Relations (3+ levels) can cause performance issues. Separate into different queries when needed.
ManyToMany Join TableSonamu automatically manages Join Tables, so you don’t need to create a separate Entity. Only separate into an Entity when additional columns are needed.

Next Steps

Enums

Define and use Enum types

Subset

Type-safe queries with Subsets

Puri Query Builder

Write queries using Relations

Performance

Optimize Relation queries