Skip to content

Prisma Relations — One-to-One, One-to-Many, and Many-to-Many

DodaTech Updated 2026-06-28 6 min read

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

  1. 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.

  2. Null foreign key without making the field optional: If the foreign key field can be null, it must be marked with ? in the schema.

  3. Circular relations without proper setup: Circular relations (A refers to B, B refers to A) need one side to be optional to create records.

  4. Missing cascade delete: Without Cascade, deleting a user with posts fails. Always consider referential actions for parent record deletion.

  5. 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

  1. 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.

  2. 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.

  3. What are the two ways to define many-to-many? Implicit (Prisma-managed join table) and explicit (custom model with @@id Composite key).

  4. What does onDelete: Cascade do? Automatically deletes all related records when the parent record is deleted.

  5. Challenge: Design a social media schema with User, Post, Comment, and Like models with appropriate relations and referential actions.

FAQ

Can a relation be optional?

Yes. Mark the foreign key field as optional (?). The parent field should also be optional.

What is the difference between implicit and explicit many-to-many?

Implicit is simpler. Explicit allows extra fields on the join table.

Can I have self-referencing relations?

Yes. A User can have a managerId referencing another User.

How do I create a record with related data?

Use nested writes: prisma.user.create({ data: { posts: { create: {...} } } }).

What happens if I omit @relation?

Prisma infers the relation name. For models with multiple relations to the same model, @relation is required.

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