Prisma Data Modeling & Migrations Deep Dive
Prisma Schema Deep Dive
The `schema.prisma` file uses Prisma Schema Language (PSL) — a clean, human-readable DSL. According to the Prisma Schema Reference, every model maps directly to a MySQL database table.
// prisma/schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
content String? @db.Text
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId Int
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}
MySQL Tip
Field Attributes
| Attribute | Purpose | MySQL Example |
|---|---|---|
| @id | Primary key | id Int @id |
| @default() | Default value | @default(autoincrement()) |
| @unique | Unique constraint | email String @unique |
| @updatedAt | Auto-update timestamp | updatedAt DateTime @updatedAt |
| @map() | Map to DB column name | @map("user_name") |
| @db.VarChar(n) | MySQL VARCHAR type | name String @db.VarChar(100) |
| @db.Text | MySQL TEXT type | content String? @db.Text |
| @db.TinyInt(1) | MySQL boolean/tinyint | active Boolean @db.TinyInt(1) |
Block Attributes
model User {
id Int @id @default(autoincrement())
email String @db.VarChar(191)
username String @db.VarChar(100)
role Role @default(USER)
@@unique([email, username]) // Composite unique constraint
@@index([email]) // Speed up email queries (MySQL index)
@@index([role, createdAt(sort: Desc)]) // Composite index with sort
@@map("users") // Maps to "users" table in MySQL
}
enum Role {
USER
ADMIN
MODERATOR
}
MySQL Note
Pro Tip
`@@index` is crucial for performance. Add indexes on any field used in `WHERE` clauses or `ORDER BY`. (prisma.io/docs/concepts/components/prisma-schema/indexes)
Relations
One-to-Many (Most Common)
model User {
id Int @id @default(autoincrement())
email String @unique @db.VarChar(191)
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
authorId Int
author User @relation(fields: [authorId], references: [id],
onDelete: Cascade)
@@index([authorId]) // ← Always index foreign keys in MySQL!
@@map("posts")
}
MySQL Best Practice
One-to-One
model User {
id Int @id @default(autoincrement())
profile Profile?
@@map("users")
}
model Profile {
id Int @id @default(autoincrement())
bio String @db.Text
userId Int @unique
user User @relation(fields: [userId], references: [id],
onDelete: Cascade)
@@map("profiles")
}
Many-to-Many (Implicit)
model Post {
id Int @id @default(autoincrement())
tags Tag[]
@@map("posts")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique @db.VarChar(100)
posts Post[]
@@map("tags")
}
Tip
Prisma Migrate vs `db push`
prisma db push |
prisma migrate dev |
|
|---|---|---|
| Use For | Prototyping / local dev | Production / team projects |
| Migration Files | Not created | Created & versioned |
| Data Loss Risk | May reset DB | Safe, reviewable SQL |
| CI/CD Ready | No | Yes |
| MySQL Support | Full | Full |
Create and apply your first migration:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "mysql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
content String? @db.Text
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId Int
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}
// migration
// npx prisma migrate dev --name init
This creates `prisma/migrations/[timestamp]_init/migration.sql` — review it:
-- CreateTable
CREATE TABLE `users` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`email` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `users_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Seeding the Database
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Upsert to avoid duplicate errors on repeated seed runs
const alice = await prisma.user.upsert({
where: { email: '[email protected]' },
update: {},
create: {
email: '[email protected]',
name: 'Alice Johnson',
posts: {
create: [
{ title: 'Hello Prisma!', published: true },
{ title: 'Getting Started with MySQL' },
],
},
},
});
console.log('Seeded:', alice);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
"scripts": {
"seed": "ts-node --project tsconfig.prisma.json prisma/seed.ts"
}
npm run seed
Explore project snapshots or discuss custom web solutions.
Prisma Studio
npx prisma studio
# Opens at http://localhost:5555
# Port might be different. Use port based on your output
Give me six hours to chop down a tree and I will spend the first four sharpening the axe.
Frequently Asked Questions
Prisma Migrate only applies additive changes automatically. If a migration would cause data loss (e.g., dropping a column), it warns you and may reset your dev DB. Production uses `prisma migrate deploy` — which never resets data.
Yes! Use `prisma db pull` to introspect your existing MySQL database and generate a `schema.prisma` automatically.
Prisma handles this automatically — all generated tables use `utf8mb4_unicode_ci`. If you're using an existing database, ensure your MySQL server is configured with `character_set_server = utf8mb4`.
Yes. MySQL natively supports ENUM columns. Define: `enum Role { USER ADMIN }` in `schema.prisma` and Prisma maps it to a MySQL `ENUM` column.
Use `@map()` to decouple your Prisma field name from the MySQL column name: `firstName String @map("first_name")`. This lets you rename in your code without touching the DB column — avoiding destructive migrations.
Comments are closed