Multiple Relations
Using @key to disambiguate multiple relations between the same models
When two models need more than one relation between them, Cerial needs a way to tell which forward relation corresponds to which reverse relation. The @key decorator solves this — it pairs each forward relation with its matching reverse.
Schema Definition
model Document {
id Record @id
title String
authorId Record
author Relation @field(authorId) @model(Writer) @key(author)
reviewerId Record? @nullable
reviewer Relation? @field(reviewerId) @model(Writer) @key(reviewer)
}
model Writer {
id Record @id
name String
authoredDocs Relation[] @model(Document) @key(author)
reviewedDocs Relation[] @model(Document) @key(reviewer)
}Each @key value links a forward relation to its corresponding reverse. Without @key, Cerial cannot determine which Writer reverse relation belongs to which Document forward relation, and the schema validator will report an error.
How @key Pairing Works
The @key value must match between a forward relation and its corresponding reverse relation on the target model:
Document.authorwith@key(author)pairs withWriter.authoredDocswith@key(author)Document.reviewerwith@key(reviewer)pairs withWriter.reviewedDocswith@key(reviewer)
Rules:
@keyvalues must be unique per model pair per direction — you cannot have two forward relations fromDocumenttoWriterwith the same@keyvalue.- If a forward relation has
@key(X), the target model must have a reverse relation with@key(X). Missing the reverse is an error. @keyis only required when multiple relations exist between the same pair of models. Single relations between two models don't need it.
If you add a second relation between two models that previously had only one, both relations now need @key — including the original one. The schema validator will tell you which fields need the decorator.
Creating Documents
// Create a document with both author and reviewer
const doc = await client.db.Document.create({
data: {
title: 'RFC: New API Design',
author: { connect: writer1Id },
reviewer: { connect: writer2Id },
},
});
// Create a document with only an author (reviewer is optional)
const draft = await client.db.Document.create({
data: {
title: 'Draft Proposal',
author: { connect: writer1Id },
},
});
// Create with nested create for the author
const docWithNewAuthor = await client.db.Document.create({
data: {
title: 'New Article',
author: {
create: { name: 'Alice' },
},
},
});Querying
From the Document Side
const doc = await client.db.Document.findOne({
where: { id: docId },
include: {
author: true,
reviewer: true,
},
});
// doc.author: Writer (required relation — always present)
// doc.reviewer: Writer | null (optional relation)From the Writer Side
const writer = await client.db.Writer.findOne({
where: { id: writerId },
include: {
authoredDocs: true,
reviewedDocs: true,
},
});
// writer.authoredDocs: Document[]
// writer.reviewedDocs: Document[]Include with Options
const writer = await client.db.Writer.findOne({
where: { id: writerId },
include: {
authoredDocs: {
orderBy: { title: 'asc' },
limit: 10,
},
reviewedDocs: {
limit: 5,
},
},
});Filtering
// Find writers who have authored at least one document
const activeAuthors = await client.db.Writer.findMany({
where: {
authoredDocs: { some: {} },
},
});
// Find writers who reviewed every document they were assigned
const thoroughReviewers = await client.db.Writer.findMany({
where: {
reviewedDocs: { every: { title: { neq: '' } } },
},
});
// Find documents by a specific author
const docs = await client.db.Document.findMany({
where: { authorId: writerId },
});
// Find documents reviewed by a specific writer
const reviewed = await client.db.Document.findMany({
where: { reviewerId: writerId },
});More Than Two Relations
You can define any number of relations between the same models. Each one needs a unique @key value:
model Task {
id Record @id
title String
creatorId Record
creator Relation @field(creatorId) @model(TeamMember) @key(creator)
assigneeId Record?
assignee Relation? @field(assigneeId) @model(TeamMember) @key(assignee)
reviewerIds Record[]
reviewers Relation[] @field(reviewerIds) @model(TeamMember) @key(reviewers)
}
model TeamMember {
id Record @id
name String
createdTasks Relation[] @model(Task) @key(creator)
assignedTasks Relation[] @model(Task) @key(assignee)
reviewingTasks Relation[] @model(Task) @key(reviewers)
}// Create a task with all three roles
const task = await client.db.Task.create({
data: {
title: 'Implement feature X',
creator: { connect: aliceId },
assignee: { connect: bobId },
reviewers: { connect: [charlieId, dianaId] },
},
});
// Query a member with all their task relations
const member = await client.db.TeamMember.findOne({
where: { id: aliceId },
include: {
createdTasks: true,
assignedTasks: true,
reviewingTasks: true,
},
});This mixes relation cardinalities — creator is required 1
assignee is optional 1, and reviewers is a non-bidirectional array (one-directional many). Each relation is independently managed with its own @key.
Delete Behavior with Multiple Relations
Each relation can have different delete behavior depending on whether the FK is required or optional:
| Relation | FK | On Writer Deletion |
|---|---|---|
author | authorId Record (required) | Cascade — documents by this author are deleted |
reviewer | reviewerId Record? @nullable (optional) | SetNull — reviewerId is set to null |
You can override the defaults with @onDelete on each relation independently:
model Document {
id Record @id
title String
authorId Record
author Relation @field(authorId) @model(Writer) @key(author)
reviewerId Record?
reviewer Relation? @field(reviewerId) @model(Writer) @key(reviewer) @onDelete(Cascade)
}See Delete Behavior for all @onDelete options.
@key with Self-Referential Relations
Self-referential models also use @key when they have multiple relations to themselves. See Self-Referential Relations for patterns like tree hierarchies, mentorship, and following systems that combine @key with self-referencing.