Relations
Relations connect models to each other and let you query across tables. ferriorm supports one-to-one, one-to-many, and implicit many-to-many relationships (via join models).
Key Concepts
Before looking at examples, there are a few things to understand about how relations work in ferriorm:
-
The side with
@relationstores the foreign key. The@relationattribute specifies which field(s) on the current model hold the foreign key and which field(s) on the related model they reference. -
Relation fields are virtual. A field whose type is another model (e.g.,
author User) does not create a database column. It tells the code generator to produce a relation accessor. The actual foreign key is stored in a separate scalar field (e.g.,authorId String). -
Both sides must be defined. The model that owns the foreign key has the
@relationattribute; the other model has a back-relation field (plain model type or list type) with no@relation.
One-to-Many
The most common relation type. One record in the parent model is related to many records in the child model.
model User {
id String @id @default(uuid())
posts Post[]
}
model Post {
id String @id @default(uuid())
author User @relation(fields: [authorId], references: [id])
authorId String
}
What this means:
| Part | Purpose |
|---|---|
posts Post[] on User | Back-relation (virtual). A user has many posts. Not a database column. |
author User @relation(...) on Post | Relation field (virtual). Represents the related user. Not a database column. |
authorId String on Post | Foreign key (real column). Stores the User.id value in the database. |
The fields: [authorId] argument tells ferriorm that Post.authorId is the foreign key, and references: [id] says it points to User.id.
Generated SQL (PostgreSQL)
CREATE TABLE "users" (
"id" TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE "posts" (
"id" TEXT NOT NULL PRIMARY KEY,
"authorId" TEXT NOT NULL REFERENCES "users"("id")
);
One-to-One
A one-to-one relation is like a one-to-many except the foreign key side has a @unique constraint, ensuring only one related record can exist.
model User {
id String @id @default(uuid())
profile Profile?
}
model Profile {
id String @id @default(uuid())
bio String?
user User @relation(fields: [userId], references: [id])
userId String @unique
}
What makes this one-to-one:
Profile.userIdhas@unique, so each user can have at most one profile.User.profileis typed asProfile?(optional), because a user may or may not have a profile.
Generated SQL (PostgreSQL)
CREATE TABLE "users" (
"id" TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE "profiles" (
"id" TEXT NOT NULL PRIMARY KEY,
"bio" TEXT,
"userId" TEXT NOT NULL UNIQUE REFERENCES "users"("id")
);
Many-to-Many (via Join Model)
For many-to-many relationships, create an explicit join model with two foreign keys:
model Post {
id String @id @default(uuid())
tags PostTag[]
}
model Tag {
id String @id @default(uuid())
name String @unique
posts PostTag[]
}
model PostTag {
post Post @relation(fields: [postId], references: [id])
postId String
tag Tag @relation(fields: [tagId], references: [id])
tagId String
@@id([postId, tagId])
}
The PostTag join model:
- Has two foreign keys (
postIdandtagId) linking toPostandTag - Uses
@@id([postId, tagId])to create a composite primary key - Is a real table in the database
Referential Actions
Referential actions control what happens when a referenced record is deleted or updated. Specify them with the onDelete and onUpdate arguments in @relation.
model Post {
id String @id @default(uuid())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
}
Available Actions
| Action | On Delete | On Update |
|---|---|---|
Cascade | Delete all posts when the user is deleted | Update foreign keys when the user’s id changes |
Restrict | Prevent deleting a user who has posts | Prevent updating a user’s id if posts reference it |
NoAction | Similar to Restrict (exact behavior is database-dependent) | Similar to Restrict |
SetNull | Set authorId to NULL when the user is deleted (field must be optional) | Set authorId to NULL when the user’s id changes |
SetDefault | Set authorId to its default value when the user is deleted | Set authorId to its default value when the user’s id changes |
Example: Cascade Delete
When a user is deleted, all their posts are automatically deleted:
model User {
id String @id @default(uuid())
posts Post[]
}
model Post {
id String @id @default(uuid())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
}
Example: Set Null
When a user is deleted, posts are kept but authorId is set to NULL:
model User {
id String @id @default(uuid())
posts Post[]
}
model Post {
id String @id @default(uuid())
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
authorId String?
}
Note that both author and authorId must be optional (?) for SetNull to work.
Self-Relations
A model can relate to itself. For example, a tree structure where each category can have a parent:
model Category {
id String @id @default(uuid())
name String
parent Category? @relation(fields: [parentId], references: [id])
parentId String?
children Category[]
}
A simple self-reference like this — one forward FK + one back-reference list — does not need a relation name. The validator can pair the two fields unambiguously.
Multiple Relations Between The Same Models
When two relations connect the same pair of models, both sides must carry a relation name so the validator and code generator can pair forward and back references correctly.
model User {
id String @id @default(uuid())
authored Post[] @relation("Authored")
reviewed Post[] @relation("Reviewed")
}
model Post {
id String @id @default(uuid())
title String
authorId String
reviewerId String
author User @relation("Authored", fields: [authorId], references: [id])
reviewer User @relation("Reviewed", fields: [reviewerId], references: [id])
}
The first positional argument to @relation is the relation name. The same name must appear on both sides — Authored pairs User.authored with Post.author; Reviewed pairs User.reviewed with Post.reviewer.
Both forms below are equivalent:
author User @relation("Authored", fields: [authorId], references: [id])
author User @relation(name: "Authored", fields: [authorId], references: [id])
If you forget to add a name where one is required, the validator reports the error:
Multiple relations from
PosttoUserrequire disambiguation. Add@relation("<Name>", ...)to each related field on both sides.
The same rule applies to multiple self-references: a model with two FKs to itself (e.g. an org chart with both a manager and a mentor pointing at User) needs distinct relation names.
Rules and Constraints
- Every
@relationmust specifyfieldsandreferences. - The
fieldsarray lists foreign key fields on the current model. - The
referencesarray lists the corresponding key fields on the related model. - The number of entries in
fieldsandreferencesmust match. - Relation fields (model-typed fields) are not stored in the database. Only the scalar foreign key fields become columns.
- The back-relation side (the side without
@relation) uses either a list type (Post[]) for one-to-many or an optional type (Profile?) for one-to-one.