Головна / Статті

Prisma без Prisma Client: типобезпечні SQL-запити в Next.js

Дізнайтеся, як використовувати Prisma для роботи зі схемами, генерувати типи Kysely за допомогою prisma-kysely, обирати діалект Neon або pg та безпечно виконувати запити з коду сервера Next.js.

Видавець: REACTAPP.TOP6 хв читання1411 слів

Джерела: **Using Prisma for Schema and Kysely for Queries in a Next.js App**

Prisma чудово підходить для проектування схем, але Prisma Client є великою залежністю, яку потрібно додавати до кожної безсерверної функції, коли все, що потрібно — це SQL із типовими перевірками. Практичний підхід: Prisma керує схемою під час розробки, генератор перетворює її на типи TypeScript, а Kysely виконує запити у продакшені. Цей посібник описує, як налаштувати це у проекті Next.js App Router, та порівнює діалект Neon HTTP із класичним пулом pg.

Якщо ви хочете звичайний підхід із Prisma Client у час виконання, дивіться налаштування Prisma 7 у проекті Next.js 16.

Як розподіляються обов’язки

Кожен інструмент у цій конфігурації має одну чітку функцію:

  • Prisma відповідає за проектування бази даних: файл схеми, синхронізацію чи міграцію бази даних, а також інструменти на кшталт Prisma Studio для перегляду даних.
  • Kysely — це засіб для створення запитів під час виконання. Ви пишете запити у форматі SQL на TypeScript, компілятор перевіряє назви таблиць та стовпців, а сама бібліотека є невеликою.
  • prisma-kysely виступає посередником: це генератор Prisma, який створює визначення типів Kysely безпосередньо з вашої схеми Prisma.

Prisma залишається частиною вашого робочого процесу розробника, тоді як Kysely є єдиним шаром бази даних у розгорнутому коді. Це має свою ціну: ви втрачаєте API Prisma Client, здатне керувати взаємозв’язками, таке як вбудовані операції include та вбудовані записи, а також можливість самостійного створення з’єднань для запису даних. Якщо ви все ще обираєте шар для роботи з даними, порівняння raw SQL, Prisma та Drizzle допоможе у прийнятті рішення.

Встановлення пакетів

Для роботи програми потрібні Kysely разом із діалектом та драйвером; це встановлює стек, заснований на Neon (тут використовується pnpm, але підходять також npm чи yarn):

pnpm add kysely kysely-neon @neondatabase/serverless
pnpm add -D prisma prisma-kysely

Зауважте, що prisma та prisma-kysely є залежностями для розробки: вони лише керують схемою та генерують код.

Визначення схеми та генерація типів Kysely

Схема є звичайною Prisma, за винятком того, що генератор — це prisma-kysely (який записує код у ./types), а не звичайний клієнтський генератор. Вона визначає типи User та Post із зв’язком один-до-багатьох.

// db/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator kysely {
  provider = "prisma-kysely"
  output   = "./types"
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String  @unique
  posts Post[]
}
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  authorId  Int
  createdAt DateTime @default(now())
  author    User     @relation(fields: [authorId], references: [id])
}

prisma generate тепер створює типи Kysely замість клієнтського коду, а prisma db push застосовує схему до бази даних:

pnpm prisma generate
pnpm prisma db push

Результатом є файл на кшталт db/types/kysely.d.ts, у якому знаходиться інтерфейс DB, що описує кожну таблицю. Є три застереження:

  • Схема знаходиться у файлі db/schema.prisma, а не у стандартному для Prisma файлі prisma/schema.prisma, тож необхідно вказати CLI на цей файл (наприклад, за допомогою параметра --schema) відповідно до вимог вашої версії Prisma.
  • db push синхронізує базу даних без збереження файлів міграцій. Для продакшн-даних prisma migrate dev та prisma migrate deploy створюють версіоновані міграції, не змінюючи решту процесу роботи.
  • У новіших версіях Prisma налаштування підключення переміщуються до окремого файлу конфігурації; якщо рядок url відхиляється, перевірте актуальну документацію.
  • Скрипти пакету для щоденної роботи

    Кілька скриптів роблять процес роботи зрозумілим для команди:

    {
      "scripts": {
        "db:push": "prisma db push",
        "db:generate": "prisma generate",
        "db:studio": "prisma studio"
      }
    }
    

    З їх наявністю зміна схеми зводиться до двох команд:

    # To generate the types
    pnpm db:generate
    # To push to the remote DB
    pnpm db:push
    

    Тоді база даних та типи Kysely залишаються узгодженими, тож компілятор позначає будь-які запити, що стосуються перейменованого чи видаленого стовпця.

    Вибір діалекту Kysely

    Kysely не спілкується безпосередньо з базою даних. Між конструктором запитів та драйвером знаходиться діалект, який знає, як компілювати та надсилати запити для певної бази даних та способу підключення. Офіційні діалекти підтримують PostgreSQL, MySQL та SQLite; громадські діалекти — сервіси на кшталт Neon, PlanetScale та Turso. Для Postgres існують два варіанти, які покривають більшість випадків.

    Neon через HTTP для безсерверних розгортань

    На Vercel або подібній платформі HTTP-драйвер Neon добре підходить: кожен запит є безстановим, тому між короткими викликами не потрібно підтримувати постійне з’єднання. Налаштування полягає у обгортанні neon() у NeonDialect та присвоєнні Kysely типу DB, створеного автоматично:

    // db/index.ts
    import { Kysely } from 'kysely'
    import { NeonDialect } from 'kysely-neon'
    import { neon } from '@neondatabase/serverless'
    import type { DB } from './types/kysely'
    
    const db = new Kysely<DB>({
      dialect: new NeonDialect({
        neon: neon(process.env.DATABASE_URL!),
      }),
    })
    
    export default db
    

    ! лише приглушує роботу TypeScript; він нічого не перевіряє. Більшою обмеженням є те, що kysely-neon через HTTP не може виконувати транзакції чи запити, засновані на сеансах, тому він підходить для простих операцій CRUD та сторінок з великою кількістю читань, а не для роботи з кількома запитами. З використанням драйвера WebSocket від Neon вам зовсім не потрібен kysely-neon: передайте Pool від Neon до вбудованого PostgresDialect у Kysely. API kysely-neon також змінювався між версіями, тому порівняйте його з поточним README.

    node-postgres для серверів, що працюють довго

    Для сервера Node.js, який працює тривалий час, офіційний драйвер pg є найбільш повним вибором – він підтримує транзакції, пулінг та підготовлені запити. Він інтегрується з власним PostgresDialect у Kysely:

    // db/index.ts
    import { Kysely, PostgresDialect } from 'kysely'
    import { Pool } from 'pg'
    import type { DB } from './types/kysely'
    
    const db = new Kysely<DB>({
      dialect: new PostgresDialect({
        pool: new Pool({
          connectionString: process.env.DATABASE_URL,
        }),
      }),
    })
    
    export default db
    

    Вибір між ними

    Різниця полягає у моделі підключень. kysely-neon — це безстановий HTTP: легкий та придатний для серверів без обслуговування, але без підтримки транзакцій. pg підтримує відкриті TCP-з’єднання у пулі з повним набором функцій, що підходить для серверів, які постійно працюють. Використовуйте kysely-neon на платформах без обслуговування, таких як Vercel чи Netlify, а pg або пул WebSocket Neon — коли вам знадобляться транзакції. Остерігайтеся використання пулу pg всередині функцій без обслуговування: кожен екземпляр відкриває власні з’єднання та може вичерпати ліміт бази даних без зовнішнього пулера.

    Запити з компонента сервера

    Будь-який код з боку сервера може імпортувати db. Цей компонент сервера відображає список користувачів, вибираючи три стовпці, впорядковані за id:

    // app/users/page.tsx
    import db from '@/db'
    
    export default async function UsersPage() {
      const users = await db
        .selectFrom('user')
        .select(['id', 'name', 'email'])
        .orderBy('id')
        .execute()
    
      return (
        <div>
          <h1>Users</h1>
          <ul>
            {users.map(u => (
              <li key={u.id}>{u.name} ({u.email})</li>
            ))}
          </ul>
        </div>
      )
    }
    

    Оскільки DB походить зі схеми Prisma, неправильно написана таблиця чи стовпець призводить до невдачі перевірки типу, а users формується саме з обраних полів.

    Є одна проблема: Prisma за замовчуванням називає таблиці на основі моделей, тому ця схема створює таблицю User, і генеровані типи використовують саме цю назву. selectFrom('user') здійснює перевірку типу лише у разі додавання @@map("user") до моделі; інакше запит виконується до 'User'. Також не слід використовувати db у клієнтських компонентах, оскільки це дозволяє отримати секрет, доступний лише на сервері.

    Щоденна робота

    Як тільки все налаштовано, цикл виглядає так:

    1. Змінюйте моделі у файлі db/schema.prisma.
    2. Поново генеруйте типи та застосовуйте зміни за допомогою двох скриптів.
    3. Використовуйте db у серверських компонентах, обробниках маршрутів чи серверських діях.
  • Перевіряйте вхідні дані за допомогою Zod або подібних інструментів перед вставкою; типи Kysely перевіряють структуру запиту, а не значення форми.
  • Отриманий макет зберігає все, що стосується бази даних, під каталогом db/:

    📦 my-next-app
    ├── 📁 app
    │   └── 📁 users
    │       └── page.tsx
    ├── 📁 db
    │   ├── schema.prisma
    │   ├── index.ts
    │   └── 📁 types
    │       └── kysely.d.ts
    ├── 📁 node_modules
    ├── .env
    ├── package.json
    ├── pnpm-lock.yaml
    └── tsconfig.json
    

    Підсумок

    Такий підхід дозволяє зберегти робочий процес схеми Prisma та Studio без використання Prisma Client, а також надає невеликий шар запитів у форматі SQL, типи якого відстежують схему.

    • Зберігайте prisma та prisma-kysely як залежності для розробки; у продакшен відправляються лише Kysely, діалект та драйвер.
    • Використовуйте діалект Neon HTTP для простих безсерверних завдань, а при необхідності транзакцій — перейдіть на діалект PostgresDialect з пулом з’єднань.
    • Звертайте увагу на найменування таблиць: генеровані типи відповідають назвам баз даних, які за замовчуванням є назвами моделей Prisma.
  • Перейдіть від db push до належних міграцій ще до того, як дані в базі даних почнуть мати значення.