Self-Referential
Models that relate to themselves — trees, hierarchies, social graphs, and mentorship patterns.
Self-referential relations allow a model to reference itself. These are useful for tree structures, social graphs, organizational hierarchies, and other recursive data patterns.
Tree Structure (1 Self-Reference)
The most common self-referential pattern — each record has an optional parent and multiple children. Used for categories, comments, org charts, and nested menus.
model Category {
id Record @id
name String
parentId Record? @nullable
parent Relation? @field(parentId) @model(Category) @key(hierarchy)
children Relation[] @model(Category) @key(hierarchy)
}The @key(hierarchy) decorator pairs the parent forward relation with the children reverse relation. Since both point to the same model, @key tells Cerial which reverse belongs to which forward.
Usage
// Create a root category
const root = await client.db.Category.create({
data: { name: 'Electronics' },
});
// Create child categories
const phones = await client.db.Category.create({
data: {
name: 'Phones',
parent: { connect: root.id },
},
});
// Create with nested children
const books = await client.db.Category.create({
data: {
name: 'Books',
children: {
create: [{ name: 'Fiction' }, { name: 'Non-Fiction' }],
},
},
});
// Query a category with its children
const category = await client.db.Category.findOne({
where: { id: root.id },
include: { children: true },
});
// category.children: Category[]
// Query a category with its parent
const child = await client.db.Category.findOne({
where: { id: phones.id },
include: { parent: true },
});
// child.parent: Category | nullOne-Directional 1
When you only need to reference another record of the same type without a reverse lookup. For example, an employee with a mentor:
model Employee {
id Record @id
name String
mentorId Record? @nullable
mentor Relation? @field(mentorId) @model(Employee)
}No @key is needed here because there is only one forward relation and no reverse relation to disambiguate.
Usage
const senior = await client.db.Employee.create({
data: { name: 'Alice' },
});
const junior = await client.db.Employee.create({
data: {
name: 'Bob',
mentor: { connect: senior.id },
},
});
const employee = await client.db.Employee.findOne({
where: { id: junior.id },
include: { mentor: true },
});
// employee.mentor: Employee | nullBidirectional 1 with @key
When you need both directions of a 1
self-reference — for example, a mentor/mentee relationship where you can query from either side:model Employee {
id Record @id
name String
mentorId Record? @nullable
mentor Relation? @field(mentorId) @model(Employee) @key(mentorship)
mentee Relation? @model(Employee) @key(mentorship)
}Usage
const mentor = await client.db.Employee.create({
data: { name: 'Alice' },
});
const mentee = await client.db.Employee.create({
data: {
name: 'Bob',
mentor: { connect: mentor.id },
},
});
// Query from mentor side
const alice = await client.db.Employee.findOne({
where: { id: mentor.id },
include: { mentee: true },
});
// alice.mentee: Employee | null
// Query from mentee side
const bob = await client.db.Employee.findOne({
where: { id: mentee.id },
include: { mentor: true },
});
// bob.mentor: Employee | nullFollowing Pattern (Single-Sided N)
A social media "following" pattern where one side stores the array of IDs but no reverse relation is defined. This is a one-directional N
— you track who you follow, but querying "who follows me" requires a manual filter.model SocialUser {
id Record @id
name String
followingIds Record[]
following Relation[] @field(followingIds) @model(SocialUser)
}Usage
const alice = await client.db.SocialUser.create({
data: { name: 'Alice' },
});
const bob = await client.db.SocialUser.create({
data: {
name: 'Bob',
following: { connect: [alice.id] },
},
});
// Query who Bob follows
const bobWithFollowing = await client.db.SocialUser.findOne({
where: { id: bob.id },
include: { following: true },
});
// bobWithFollowing.following: SocialUser[]
// Find followers of Alice (manual reverse query via FK)
const followers = await client.db.SocialUser.findMany({
where: { followingIds: { has: alice.id } },
});Since there's no reverse Relation[] on the target side, no bidirectional sync occurs. If Alice unfollows someone, only Alice's followingIds is updated.
Symmetric N (Friends Pattern)
A friends list where the relationship is mutual. Both sides use the same Record[] + Relation[] field, so Cerial recognizes this as a symmetric N
model Person {
id Record @id
name String
friendIds Record[]
friends Relation[] @field(friendIds) @model(Person)
}Because the forward relation points to the same model with the same field, adding person A as a friend of person B also adds person B as a friend of person A.
Usage
const alice = await client.db.Person.create({
data: { name: 'Alice' },
});
const bob = await client.db.Person.create({
data: {
name: 'Bob',
friends: { connect: [alice.id] },
},
});
// Bob.friendIds = [alice.id]
// Alice.friendIds += [bob.id] (automatic sync)
// Query friends
const person = await client.db.Person.findOne({
where: { id: alice.id },
include: { friends: true },
});
// person.friends: Person[] — includes BobDisconnect also syncs both sides:
await client.db.Person.updateMany({
where: { id: bob.id },
data: { friends: { disconnect: [alice.id] } },
});
// Bob.friendIds no longer contains alice.id
// Alice.friendIds no longer contains bob.idWhen is @key Required?
@key is required when a model has multiple forward relations or multiple reverse relations pointing to itself. The validator needs @key to determine which forward pairs with which reverse.
| Scenario | @key Required? |
|---|---|
| Single forward, no reverse | No |
| Single forward + single reverse | Recommended for clarity, but not enforced |
| Multiple forwards | Yes — each forward needs a unique @key |
| Multiple reverses | Yes — each reverse needs a unique @key |
| Multiple forwards + multiple reverses | Yes — keys pair each forward with its reverse |
| N self-reference (symmetric) | No — single relation, auto-syncs with itself |
Even when not strictly required, adding @key to a single forward + single reverse pair makes the schema more readable and future-proof. If you later add a second relation to the same model, you'll need @key anyway.
@key pairing rules
- The
@keyvalue on a forward relation must match the@keyvalue on its corresponding reverse relation. @keyvalues must be unique among forward relations to the same target, and unique among reverse relations from the same target.- Missing or mismatched
@keyvalues produce a validation error at generation time.
See Multiple Relations for non-self-referential @key usage.
Patterns Summary
| Pattern | Schema Shape | Sync Behavior |
|---|---|---|
| Tree (1) | Record? + Relation? @key + Relation[] @key | No sync (FK-based) |
| One-directional 1 | Record? + Relation? | No sync |
| Bidirectional 1 | Record? + Relation? @key + Relation? @key | No sync (FK-based) |
| Following (one-sided N) | Record[] + Relation[] | No sync |
| Friends (symmetric N) | Record[] + Relation[] (same model) | Automatic sync |