Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. The side with @relation stores the foreign key. The @relation attribute specifies which field(s) on the current model hold the foreign key and which field(s) on the related model they reference.

  2. 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).

  3. Both sides must be defined. The model that owns the foreign key has the @relation attribute; 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:

PartPurpose
posts Post[] on UserBack-relation (virtual). A user has many posts. Not a database column.
author User @relation(...) on PostRelation field (virtual). Represents the related user. Not a database column.
authorId String on PostForeign 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.userId has @unique, so each user can have at most one profile.
  • User.profile is typed as Profile? (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 (postId and tagId) linking to Post and Tag
  • 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

ActionOn DeleteOn Update
CascadeDelete all posts when the user is deletedUpdate foreign keys when the user’s id changes
RestrictPrevent deleting a user who has postsPrevent updating a user’s id if posts reference it
NoActionSimilar to Restrict (exact behavior is database-dependent)Similar to Restrict
SetNullSet authorId to NULL when the user is deleted (field must be optional)Set authorId to NULL when the user’s id changes
SetDefaultSet authorId to its default value when the user is deletedSet 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 Post to User require 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 @relation must specify fields and references.
  • The fields array lists foreign key fields on the current model.
  • The references array lists the corresponding key fields on the related model.
  • The number of entries in fields and references must 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.