Prisma Relations — One-to-One, One-to-Many, and Many-to-Many
In this tutorial, you will learn about Prisma Relations. We cover key concepts, practical examples, and best practices to help you master this topic.
Prisma relations connect models using foreign keys, supporting one-to-one, one-to-many, and many-to-many relationships with referential actions (cascade, restrict, set null) for data integrity.
What You'll Learn
By the end of this lesson you will define all three relation types, configure referential actions on delete and update, create self-relations for hierarchies, and efficiently load related data.
Why It Matters
Relationships are the heart of relational databases. Correctly modeling relations ensures data integrity, enables efficient queries, and prevents orphaned records or unintended deletions.
Real-World Use
DodaZIP's User model has a one-to-many relation to File, a one-to-one relation to Profile, and a many-to-many relation to Team via a join table. Cascade deletes ensure cleanup when a user is removed.
One-to-Many Relations
The most common relation type.
// One-to-Many: User -> Post
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
}
# one_to_many.py
# One-to-many relation patterns
def one_to_many():
print("One-to-Many Relation:")
print()
print("One User has many Posts")
print("One Post belongs to one User")
print()
print("Schema pattern:")
print(" Side A (parent): relationField OtherModel[]")
print(" Side B (child): relationField OtherModel @relation(fields: [fk], references: [id])")
print(" Side B (child): fkField OtherType")
print()
print("Query examples:")
print(" // Get user with all posts")
print(" prisma.user.findUnique({")
print(' where: { id: 1 },')
print(" include: { posts: true }")
print(" })")
one_to_many()
One-to-One Relations
Unique foreign key constraints ensure one-to-one.
// One-to-One: User -> Profile
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique // Unique enforces one-to-one
user User @relation(fields: [userId], references: [id])
}
# one_to_one.py
# One-to-one relation patterns
def one_to_one():
print("One-to-One Relation:")
print()
print("One User has one Profile")
print("One Profile belongs to one User")
print()
print("Key difference from one-to-many:")
print(" The foreign key field has @unique")
print(" This ensures only one Profile per User")
print()
print("Optional vs Required:")
print(" Profile? -> Optional (user may not have profile)")
print(" Profile -> Required (user must have profile)")
one_to_one()
Many-to-Many Relations
Two ways to define many-to-many relations.
// Implicit Many-to-Many (Prisma manages the join table)
model User {
id Int @id @default(autoincrement())
name String
teams Team[]
}
model Team {
id Int @id @default(autoincrement())
name String
users User[]
}
// Explicit Many-to-Many (custom join table)
model User {
id Int @id @default(autoincrement())
name String
teamUsers TeamUser[]
}
model Team {
id Int @id @default(autoincrement())
name String
teamUsers TeamUser[]
}
model TeamUser {
user User @relation(fields: [userId], references: [id])
userId Int
team Team @relation(fields: [teamId], references: [id])
teamId Int
role String?
@@id([userId, teamId])
}
# many_to_many.py
# Many-to-many relation patterns
def many_to_many():
print("Many-to-Many Relation:")
print()
print("Implicit (automatic join table):")
print(" + Simple, no extra fields")
print(" + Prisma manages the join table")
print(" + model A { bs B[] } / model B { as A[] }")
print()
print("Explicit (manual join table):")
print(" + Allows extra fields on the relation")
print(" + Full control over join table name")
print(" + model A { bs AB[] } / model AB { a A, b B } / model B { as AB[] }")
print()
print("Choose implicit unless:")
print(" - You need extra fields on the relation")
print(" - You need to query the relation directly")
many_to_many()
Referential Actions
Control what happens when a referenced record is deleted.
model User {
id Int @id @default(autoincrement())
posts Post[]
profile Profile?
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
}
# referential_actions.py
# Referential action options
def actions():
actions = {
"Cascade": "Delete child records when parent is deleted",
"Restrict": "Prevent deleting parent if children exist",
"NoAction": "Database-level restriction (default)",
"SetNull": "Set foreign key to NULL when parent deleted",
"SetDefault": "Set foreign key to default value when parent deleted",
}
print("Referential Actions:")
for action, desc in actions.items():
print(f" {action:12s} | {desc}")
print()
print("Recommendations:")
print(" - Use Cascade for ownership relations (User -> Post)")
print(" - Use SetNull for optional references (User -> Profile)")
print(" - Use Restrict for critical data (Order -> Product)")
actions()
Common Mistakes
Forgetting the @relation attribute on one side: In one-to-many and one-to-one, the @relation attribute is required on the child model to specify the foreign key.
Null foreign key without making the field optional: If the foreign key field can be null, it must be marked with ? in the schema.
Circular relations without proper setup: Circular relations (A refers to B, B refers to A) need one side to be optional to create records.
Missing cascade delete: Without Cascade, deleting a user with posts fails. Always consider referential actions for parent record deletion.
Using implicit many-to-many with extra fields: Implicit relations cannot have extra fields. Use explicit join table if you need attributes on the relation.
Practice Questions
How do you define a one-to-many relation in Prisma? Add an array field on the parent (Post[]) and a relation field on the child with @relation.
What makes a one-to-one relation different from one-to-many? The foreign key field has @unique to enforce at most one related record.
What are the two ways to define many-to-many? Implicit (Prisma-managed join table) and explicit (custom model with @@id Composite key).
What does onDelete: Cascade do? Automatically deletes all related records when the parent record is deleted.
Challenge: Design a social media schema with User, Post, Comment, and Like models with appropriate relations and referential actions.
FAQ
Mini Project
Create a schema for a task management system with User, Project, Task, and Tag models. User has many Projects and Tasks. Project has many Tasks. Task has many Tags (many-to-many). Include cascade deletes where appropriate.
def task_manager_schema():
print("Task Manager Relations:")
print()
print("User --< Project (one user has many projects)")
print("User --< Task (one user has many tasks)")
print("Project --< Task (one project has many tasks)")
print("Task >--< Tag (many-to-many via implicit relation)")
print()
print("Referential actions:")
print(" User delete -> Cascade to Projects and Tasks")
print(" Project delete -> Cascade to Tasks")
print(" Task delete -> Remove from Tags (no cascade needed)")
task_manager_schema()
What's Next
Next: Prisma Migrations for schema evolution.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro