The Three Shapes
Every relation in a relational database is one of three shapes:
- One-to-one — a user has one profile
- One-to-many — a user has many posts
- Many-to-many — a post has many tags and a tag has many posts
Prisma has idiomatic syntax for all three. Once you see them side by side, the pattern clicks.
One-to-Many (the most common)
A User has many Post rows. The foreign key lives on the Post side.
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
The posts field on User is virtual — Prisma uses it for queries like include: { posts: true } but there's no column for it. The real data is the authorId column on Post.
One-to-One
A User has one Profile. Same shape as one-to-many, but the foreign key is marked @unique.
model User {
id Int @id @default(autoincrement())
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
The @unique on userId is what makes it one-to-one. Without it, the same user could have multiple profiles.
Many-to-Many (explicit)
A Post has many Tag rows, and each Tag is on many posts. You need a join table.
model Post {
id Int @id @default(autoincrement())
tags PostTag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts PostTag[]
}
model PostTag {
postId Int
tagId Int
post Post @relation(fields: [postId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}
The composite primary key @@id([postId, tagId]) prevents duplicates.
Querying Relations
Use include to fetch related data, select to pick fields.
// Fetch a user with their first 10 posts
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
take: 10,
orderBy: { createdAt: "desc" },
},
},
});
The N+1 Trap
A common mistake: fetching a list, then looping to fetch related data. That creates N+1 queries.
// BAD — N+1 queries
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({ where: { id: post.authorId } });
}
// GOOD — 1 query
const posts = await prisma.post.findMany({ include: { author: true } });
Always prefer include over looping.