Стандарты

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
ПолеcamelCasefirstName, createdAt
EnumPascalCaseOrderStatus, UserRole
Enum значенияSCREAMING_SNAKE_CASEPENDING, 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 pushSync без миграции (прототипирование)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

Связанные стандарты