Стандарты
Prisma Schema Conventions
Стандарты проектирования схемы Prisma и работы с базой данных.
Стандарты проектирования схемы Prisma и работы с базой данных.
Документация:
Подход
- Prisma 7 — новый генератор
prisma-clientс обязательным output - PascalCase — имена моделей в единственном числе
- camelCase — имена полей
- Явные связи — всегда указывать
@relationс полями - Мягкое удаление —
deletedAtвместо физического удаления
Конфигурация (Prisma 7)
prisma.config.ts (обязательно)
В Prisma 7 конфигурация datasource вынесена в отдельный файл:
// prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations'
},
datasource: {
url: env('DATABASE_URL')
}
})
schema.prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client" // НЕ "prisma-client-js"!
output = "../server/generated/prisma" // ОБЯЗАТЕЛЬНО!
}
datasource db {
provider = "postgresql"
// url теперь в prisma.config.ts
}
Импорт клиента
// ✅ Правильно (Prisma 7)
import { PrismaClient } from '../generated/prisma'
// ❌ Неправильно — старый синтаксис
import { PrismaClient } from '@prisma/client'
Driver Adapters (опционально)
Для edge runtime или уменьшения бандла используйте driver adapters.
Настройка
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../server/generated/prisma"
engineType = "client" // Включает режим driver adapters
}
Установка adapter
# SQLite
pnpm add @prisma/adapter-better-sqlite3 better-sqlite3
# PostgreSQL
pnpm add @prisma/adapter-pg pg
# MySQL
pnpm add @prisma/adapter-mysql2 mysql2
Инициализация с adapter
// server/utils/prisma.ts (с driver adapter)
import { PrismaClient } from '../generated/prisma'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import path from 'node:path'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
function createPrismaClient() {
const dbPath = path.resolve(process.cwd(), 'prisma/dev.db')
const adapter = new PrismaBetterSqlite3({ url: dbPath })
return new PrismaClient({ adapter })
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Когда использовать driver adapters
| Сценарий | Рекомендация |
|---|---|
| Стандартный Node.js сервер | Без adapter (проще) |
| Edge runtime (Cloudflare, Vercel Edge) | С adapter (обязательно) |
| Минимизация бандла | С adapter |
| Максимальная производительность | Без adapter (Rust engine быстрее) |
Именование моделей
Основные правила
| Что | Конвенция | Пример |
|---|---|---|
| Модель | PascalCase, ед. число | User, OrderItem |
| Поле | camelCase | firstName, createdAt |
| Enum | PascalCase | OrderStatus, UserRole |
| Enum значения | SCREAMING_SNAKE_CASE | PENDING, IN_PROGRESS |
| Таблица (@map) | snake_case, мн. число | @map("users"), @map("order_items") |
Примеры
// Модель — PascalCase, единственное число
model User {
id String @id @default(cuid())
email String @unique
firstName String @map("first_name") // camelCase в коде
lastName String @map("last_name")
role UserRole @default(USER)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
@@map("users") // snake_case, множественное число в БД
}
// Enum — PascalCase
enum UserRole {
USER
ADMIN
MODERATOR
}
// Составное имя — PascalCase слитно
model OrderItem {
id String @id @default(cuid())
quantity Int
price Decimal @db.Decimal(10, 2)
orderId String @map("order_id")
productId String @map("product_id")
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
@@map("order_items")
}
Маппинг полей в snake_case
model User {
id String @id @default(cuid())
firstName String @map("first_name") // В БД: first_name
lastName String @map("last_name") // В БД: last_name
phoneNumber String? @map("phone_number")
avatarUrl String? @map("avatar_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("users")
}
Стандартные поля
Базовые поля для всех моделей
model Entity {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("entities")
}
Soft delete
model User {
id String @id @default(cuid())
email String @unique
deletedAt DateTime? @map("deleted_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
// Запрос с учётом soft delete
const users = await prisma.user.findMany({
where: { deletedAt: null }
})
// Мягкое удаление
await prisma.user.update({
where: { id },
data: { deletedAt: new Date() }
})
ID стратегии
// ✅ Рекомендуется — cuid() для распределённых систем
id String @id @default(cuid())
// ✅ Альтернатива — uuid()
id String @id @default(uuid())
// ⚠️ Для legacy систем — auto-increment
id Int @id @default(autoincrement())
Связи (Relations)
One-to-Many
model User {
id String @id @default(cuid())
email String @unique
orders Order[]
@@map("users")
}
model Order {
id String @id @default(cuid())
userId String @map("user_id")
user User @relation(fields: [userId], references: [id])
@@map("orders")
}
Many-to-Many
// Явная связующая таблица (рекомендуется)
model Post {
id String @id @default(cuid())
title String
tags PostTag[]
@@map("posts")
}
model Tag {
id String @id @default(cuid())
name String @unique
posts PostTag[]
@@map("tags")
}
model PostTag {
postId String @map("post_id")
tagId String @map("tag_id")
assignedAt DateTime @default(now()) @map("assigned_at")
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
@@map("post_tags")
}
One-to-One
model User {
id String @id @default(cuid())
email String @unique
profile Profile?
@@map("users")
}
model Profile {
id String @id @default(cuid())
bio String?
userId String @unique @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profiles")
}
Self-relation (иерархия)
model Category {
id String @id @default(cuid())
name String
parentId String? @map("parent_id")
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
@@map("categories")
}
Каскадное удаление
model User {
id String @id @default(cuid())
orders Order[]
@@map("users")
}
model Order {
id String @id @default(cuid())
userId String @map("user_id")
// При удалении User — удалить все Order
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("orders")
}
Referential actions
| Действие | Описание |
|---|---|
Cascade | Удалить связанные записи |
Restrict | Запретить удаление при наличии связей |
SetNull | Установить NULL (поле должно быть nullable) |
NoAction | Делегировать БД (default) |
SetDefault | Установить значение по умолчанию |
Индексы
Одиночные индексы
model User {
id String @id @default(cuid())
email String @unique // Уникальный индекс
phone String?
createdAt DateTime @default(now())
@@index([createdAt]) // Обычный индекс
@@index([phone])
@@map("users")
}
Составные индексы
model Order {
id String @id @default(cuid())
userId String @map("user_id")
status OrderStatus
createdAt DateTime @default(now()) @map("created_at")
// Составной индекс для частых запросов
@@index([userId, status])
@@index([userId, createdAt])
@@map("orders")
}
Уникальные ограничения
model UserEmail {
id String @id @default(cuid())
userId String @map("user_id")
email String
isPrimary Boolean @default(false) @map("is_primary")
// Уникальная комбинация
@@unique([userId, email])
// Только один primary email на пользователя
@@unique([userId, isPrimary])
@@map("user_emails")
}
Миграции
Workflow разработки
# 1. Изменить schema.prisma
# 2. Создать миграцию (development)
pnpm prisma migrate dev --name add_phone_to_users
# 3. Применить в production
pnpm prisma migrate deploy
Команды миграций
| Команда | Описание | Окружение |
|---|---|---|
prisma migrate dev --name xxx | Создать и применить миграцию | Development |
prisma migrate deploy | Применить pending миграции | Production |
prisma migrate reset | Сбросить БД и применить заново | Development |
prisma migrate status | Показать статус миграций | Любое |
prisma db push | Sync без миграции (прототипирование) | Development |
Именование миграций
# Формат: действие_что_где
pnpm prisma migrate dev --name add_phone_to_users
pnpm prisma migrate dev --name create_orders_table
pnpm prisma migrate dev --name add_index_on_email
pnpm prisma migrate dev --name remove_deprecated_fields
Ручная миграция (SQL)
# Создать пустую миграцию
pnpm prisma migrate dev --name custom_migration --create-only
# Редактировать SQL в migrations/xxx_custom_migration/migration.sql
# Применить
pnpm prisma migrate dev
Seeding
Настройка seed
// package.json
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
Базовый seed
// prisma/seed.ts
import { PrismaClient } from '../server/generated/prisma'
const prisma = new PrismaClient()
async function main() {
console.log('Seeding database...')
// Создать admin пользователя
const admin = await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
firstName: 'Admin',
lastName: 'User',
role: 'ADMIN'
}
})
console.log({ admin })
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(() => prisma.$disconnect())
Seed с фабриками
// prisma/seed.ts
import { PrismaClient } from '../server/generated/prisma'
import { faker } from '@faker-js/faker'
const prisma = new PrismaClient()
// Фабрика пользователей
function createRandomUser() {
return {
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
phone: faker.phone.number(),
role: 'USER' as const
}
}
async function main() {
// Создать 50 тестовых пользователей
const users = await Promise.all(
Array.from({ length: 50 }).map(() =>
prisma.user.create({
data: createRandomUser()
})
)
)
console.log(`Created ${users.length} users`)
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())
Seed по окружениям
// prisma/seed.ts
async function main() {
// Базовые данные для всех окружений
await seedRoles()
await seedCategories()
// Тестовые данные только для development
if (process.env.NODE_ENV !== 'production') {
await seedTestUsers()
await seedTestOrders()
}
}
Запуск seed
# Запустить seed
pnpm prisma db seed
# После reset автоматически запускается seed
pnpm prisma migrate reset
Enums
Определение
enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
enum PaymentStatus {
PENDING
PAID
FAILED
REFUNDED
}
model Order {
id String @id @default(cuid())
status OrderStatus @default(PENDING)
paymentStatus PaymentStatus @default(PENDING) @map("payment_status")
@@map("orders")
}
Использование в TypeScript
import { OrderStatus } from '../generated/prisma'
// Тип из Prisma
const status: OrderStatus = 'PENDING'
// Все значения enum
const allStatuses = Object.values(OrderStatus)
Типы данных
Строки и текст
model Post {
id String @id @default(cuid())
title String @db.VarChar(255) // Ограниченная строка
slug String @db.VarChar(100)
content String @db.Text // Длинный текст
excerpt String? @db.VarChar(500)
@@map("posts")
}
Числа
model Product {
id String @id @default(cuid())
price Decimal @db.Decimal(10, 2) // Деньги: 10 цифр, 2 после запятой
quantity Int @default(0)
weight Float? // Дробное число
@@map("products")
}
Даты
model Event {
id String @id @default(cuid())
title String
startDate DateTime @map("start_date")
endDate DateTime @map("end_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("events")
}
JSON
model User {
id String @id @default(cuid())
preferences Json @default("{}") // Произвольные настройки
metadata Json? // Опциональные метаданные
@@map("users")
}
// Типизация JSON полей
interface UserPreferences {
theme: 'light' | 'dark'
notifications: boolean
language: string
}
const user = await prisma.user.findUnique({
where: { id }
})
const prefs = user?.preferences as UserPreferences
Gotchas (Важные нюансы)
1. Prisma 7 требует output
// ✅ Правильно
generator client {
provider = "prisma-client"
output = "../server/generated/prisma"
}
// ❌ Ошибка — output обязателен в Prisma 7
generator client {
provider = "prisma-client"
}
2. Regenerate после изменений
# После ЛЮБОГО изменения schema.prisma
pnpm prisma generate
3. .gitignore для generated
# .gitignore
server/generated/
4. Nullable vs Optional
// Nullable — может быть NULL в БД
phone String?
// Required — обязательное поле
email String
// Default — значение по умолчанию
role UserRole @default(USER)
5. Decimal для денег
// ✅ Правильно — Decimal для денег
price Decimal @db.Decimal(10, 2)
// ❌ Неправильно — Float теряет точность
price Float
6. Индексы для foreign keys
Prisma автоматически создаёт индексы для @relation, но для сложных запросов нужны составные индексы:
model Order {
id String @id @default(cuid())
userId String @map("user_id")
status OrderStatus
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
// Явный составной индекс для фильтрации
@@index([userId, status])
@@map("orders")
}
Чек-лист
- Генератор
prisma-clientс обязательнымoutput - Модели в PascalCase, поля в camelCase
-
@@map()для snake_case имён таблиц в БД -
@map()для snake_case имён полей в БД - Стандартные поля:
id,createdAt,updatedAt - Soft delete через
deletedAtгде нужно - Явные
@relationсfieldsиreferences - Индексы для часто используемых фильтров
-
Decimalдля денежных значений - Миграции с понятными именами
- Seed для базовых данных
- Generated директория в
.gitignore
Связанные стандарты
- ADR-014: Prisma ORM — архитектурное решение
- standard-backend-api — REST API с Prisma
- standard-type-generation — генерация типов
- standard-typescript — TypeScript конвенции