Relations
Many-to-One
Define many-to-one relationships between models
Many-to-One Relation
A many-to-one relation connects multiple records to a single related record. This is the "many" side that owns the foreign key.
Basic Example
import { s } from "viborm";
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
authorId: s.string(), // FK field
author: s.relation
.fields("authorId")
.references("id")
.manyToOne(() => user),
}).map("posts");
const user = s.model({
id: s.string().id().ulid(),
email: s.string().unique(),
posts: s.relation.oneToMany(() => post), // Inverse side
}).map("users");Characteristics
| Aspect | Value |
|---|---|
| Returns | Single object or null |
| FK location | On this model |
| Can be null | Yes, with .optional() |
| Required by default | Yes |
Config Methods (before .manyToOne())
.fields(...fieldNames)
Required. Specifies the FK field(s) on this model:
s.relation.fields("authorId").manyToOne(() => user)
// Composite FK
s.relation.fields("orgId", "teamId").manyToOne(() => organization).references(...fieldNames)
Required. Specifies the referenced field(s) on the target:
s.relation
.fields("authorId")
.references("id")
.manyToOne(() => user)
// Composite reference
s.relation
.fields("orgId", "teamId")
.references("id", "teamId")
.manyToOne(() => organization).optional()
Makes the relation optional (FK can be null):
// Post may or may not have an author
author: s.relation
.fields("authorId")
.references("id")
.optional()
.manyToOne(() => user)When using .optional(), the FK field should also be nullable:
authorId: s.string().nullable(),
author: s.relation
.fields("authorId")
.references("id")
.optional()
.manyToOne(() => user).onDelete(action)
Referential action when the related record is deleted:
s.relation
.fields("authorId")
.references("id")
.onDelete("cascade") // Delete posts when user is deleted
.manyToOne(() => user)| Action | Effect |
|---|---|
cascade | Delete this record too |
setNull | Set FK to null (requires .optional()) |
restrict | Prevent deletion |
noAction | Database default |
.onUpdate(action)
Referential action when the referenced field changes:
s.relation
.fields("authorId")
.references("id")
.onUpdate("cascade") // Update FK when user ID changes
.manyToOne(() => user)Complete Example
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
content: s.string(),
published: s.boolean().default(false),
// Required author
authorId: s.string(),
author: s.relation
.fields("authorId")
.references("id")
.onDelete("cascade")
.manyToOne(() => user),
// Optional category
categoryId: s.string().nullable(),
category: s.relation
.fields("categoryId")
.references("id")
.optional()
.onDelete("setNull")
.manyToOne(() => category),
}).map("posts");Querying Many-to-One
// Include author when fetching post
const post = await client.post.findUnique({
where: { id: "post_123" },
include: { author: true },
});
// post.author: User
// Filter posts by author
const posts = await client.post.findMany({
where: {
author: {
is: { role: "ADMIN" }
}
}
});
// Filter posts without author (optional relation)
const orphanedPosts = await client.post.findMany({
where: {
author: {
is: null
}
}
});
// Select specific author fields
const posts = await client.post.findMany({
select: {
title: true,
author: {
select: { name: true, email: true }
}
}
});Creating with Relations
// Connect to existing user
const post = await client.post.create({
data: {
title: "My Post",
content: "Hello!",
author: {
connect: { id: "user_123" }
}
}
});
// Or use FK directly
const post = await client.post.create({
data: {
title: "My Post",
content: "Hello!",
authorId: "user_123"
}
});
// Create and connect in one operation
const post = await client.post.create({
data: {
title: "My Post",
content: "Hello!",
author: {
connectOrCreate: {
where: { email: "alice@example.com" },
create: { email: "alice@example.com", name: "Alice" }
}
}
}
});Common Patterns
Required vs Optional
// Required: Every post MUST have an author
authorId: s.string(),
author: s.relation
.fields("authorId")
.references("id")
.manyToOne(() => user)
// Optional: Post MAY have a category
categoryId: s.string().nullable(),
category: s.relation
.fields("categoryId")
.references("id")
.optional()
.manyToOne(() => category)Self-Referential (Tree Structure)
const comment = s.model({
id: s.string().id().ulid(),
content: s.string(),
parentId: s.string().nullable(),
parent: s.relation
.fields("parentId")
.references("id")
.optional()
.manyToOne(() => comment),
replies: s.relation.oneToMany(() => comment),
}).map("comments");