Cerial

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.author with @key(author) pairs with Writer.authoredDocs with @key(author)
  • Document.reviewer with @key(reviewer) pairs with Writer.reviewedDocs with @key(reviewer)

Rules:

  • @key values must be unique per model pair per direction — you cannot have two forward relations from Document to Writer with the same @key value.
  • If a forward relation has @key(X), the target model must have a reverse relation with @key(X). Missing the reverse is an error.
  • @key is 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:

RelationFKOn Writer Deletion
authorauthorId Record (required)Cascade — documents by this author are deleted
reviewerreviewerId Record? @nullable (optional)SetNullreviewerId 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.

On this page