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 oneExample: Post β†’ User (multiple posts belong to one user)

OneToOne

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

HasMany

1:N relationship - One owns manyExample: User β†’ Posts (one user owns multiple posts)

ManyToMany

N:M relationship - Many-to-manyExample: 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