Many-to-One
Define many-to-one relationships connecting multiple records to a single related record
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.manyToOne(() => user)
.fields("authorId")
.references("id"),
});
const user = s.model({
id: s.string().id().ulid(),
email: s.string().unique(),
posts: s.oneToMany(() => post), // Inverse side
});
Characteristics
| Aspect | Value |
|---|---|
| Returns | Single object or null |
| FK location | On this model |
| Can be null | Yes, with .optional() |
| Required by default | Yes |
Configuration
Many-to-one is the FK-owning side — .fields() and .references() are required. See the relation method reference and referential actions:
s.manyToOne(() => user)
.fields("authorId") // FK field(s) on this model - required
.references("id") // Referenced field(s) on the target - required
.optional() // Relation can be null
.onDelete("cascade") // Referential action on delete
.onUpdate("cascade") // Referential action on update
.name("writer") // Custom relation name
Composite foreign keys pass multiple fields:
s.manyToOne(() => organization)
.fields("orgId", "teamId")
.references("id", "teamId")
The foreign-key index
You do not declare it. push and migrate add an index over the .fields()
columns on every database — PostgreSQL, MySQL and SQLite all leave a foreign key
unindexed otherwise, and every include, relation filter and nested write reads
this side of the relation through those columns.
What is worth declaring is a wider index, when a query needs more than the foreign key. The usual case is an include that orders and limits:
const post = s.model({
id: s.string().id().ulid(),
title: s.string(),
createdAt: s.dateTime().now(),
authorId: s.string(),
author: s.manyToOne(() => user).fields("authorId").references("id"),
}).index(["authorId", "createdAt"]);
An index whose leading columns are the foreign key replaces the automatic one rather than adding a second index over the same column — so put the foreign key first. See composite indexes for ordered includes.
One index does not count: a partial index (.index([...], { where })) holds
only the rows its predicate keeps, so it cannot serve the rows the predicate
excludes. The automatic foreign-key index is still added alongside it.
When using .optional(), the FK field itself must also be .nullable():
authorId: s.string().nullable(),
author: s.manyToOne(() => user)
.fields("authorId")
.references("id")
.optional()
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.manyToOne(() => user)
.fields("authorId")
.references("id")
.onDelete("cascade"),
// Optional category
categoryId: s.string().nullable(),
category: s.manyToOne(() => category)
.fields("categoryId")
.references("id")
.optional()
.onDelete("setNull"),
});
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
// Connect to an existing user on create (or set authorId directly)
const created = await client.post.create({
data: {
title: "My Post",
author: {
connect: { id: "user_123" }
}
}
});
See To-One Relation Filters for filter operators and Nested Writes for create/connect/connectOrCreate.
Self-Referential
The parent/child pattern — an entity pointing at another row of the same model — is a many-to-one relation with a one-to-many inverse:
// Employees and their manager
const employee = s.model({
id: s.string().id().ulid(),
name: s.string(),
managerId: s.string().nullable(),
manager: s.manyToOne(() => employee)
.fields("managerId")
.references("id")
.optional(),
reports: s.oneToMany(() => employee),
});
// Threaded comments
const comment = s.model({
id: s.string().id().ulid(),
content: s.string(),
parentId: s.string().nullable(),
parent: s.manyToOne(() => comment)
.fields("parentId")
.references("id")
.optional(),
replies: s.oneToMany(() => comment),
});