Relations
One-to-One
Define one-to-one relationships between models
One-to-One Relation
A one-to-one relation connects a single record to exactly one other record.
Basic Example
import { s } from "viborm";
// User has one profile
const user = s.model({
id: s.string().id().ulid(),
email: s.string().unique(),
profile: s.relation.optional().oneToOne(() => profile),
}).map("users");
// Profile belongs to one user
const profile = s.model({
id: s.string().id().ulid(),
bio: s.string(),
userId: s.string().unique(), // FK field, unique for 1:1
user: s.relation
.fields("userId")
.references("id")
.oneToOne(() => user),
}).map("profiles");Which Side Owns the FK?
In a one-to-one relation, one side must have the foreign key:
| Side | Has FK | Configuration |
|---|---|---|
| Owner (Profile) | Yes | .fields("userId").references("id") before .oneToOne() |
| Non-owner (User) | No | Just .optional() before .oneToOne() or nothing |
Config Methods (before .oneToOne())
.fields(...fieldNames)
Specifies the FK field(s) on the current model:
s.relation.fields("userId").oneToOne(() => user).references(...fieldNames)
Specifies the referenced field(s) on the target model:
s.relation
.fields("userId")
.references("id")
.oneToOne(() => user).optional()
Makes the relation optional (can be null):
// User may or may not have a profile
profile: s.relation.optional().oneToOne(() => profile).onDelete(action)
Referential action when the related record is deleted:
s.relation
.fields("userId")
.references("id")
.onDelete("cascade") // Delete profile when user is deleted
.oneToOne(() => user).onUpdate(action)
Referential action when the referenced field is updated:
s.relation
.fields("userId")
.references("id")
.onUpdate("cascade")
.oneToOne(() => user)Complete Example
const user = s.model({
id: s.string().id().ulid(),
email: s.string().unique(),
// Non-owning side - no FK, can be optional
profile: s.relation.optional().oneToOne(() => profile),
}).map("users");
const profile = s.model({
id: s.string().id().ulid(),
bio: s.string().nullable(),
avatar: s.string().nullable(),
// FK field - unique for 1:1
userId: s.string().unique(),
// Owning side - has FK configuration
user: s.relation
.fields("userId")
.references("id")
.onDelete("cascade")
.oneToOne(() => user),
}).map("profiles");Querying One-to-One
// Include profile when fetching user
const user = await client.user.findUnique({
where: { id: "user_123" },
include: { profile: true },
});
// user.profile: Profile | null
// Filter users by profile data
const users = await client.user.findMany({
where: {
profile: {
is: { bio: { contains: "developer" } }
}
}
});
// Create user with profile
const user = await client.user.create({
data: {
email: "alice@example.com",
profile: {
create: { bio: "Software developer" }
}
},
include: { profile: true },
});Common Patterns
Required vs Optional
// Required: Every user MUST have a profile
// (Create profile in same transaction)
profile: s.relation.oneToOne(() => profile)
// Optional: User MAY have a profile
profile: s.relation.optional().oneToOne(() => profile)Self-Referential
const employee = s.model({
id: s.string().id().ulid(),
name: s.string(),
managerId: s.string().nullable(),
manager: s.relation
.fields("managerId")
.references("id")
.optional()
.oneToOne(() => employee),
}).map("employees");