Prisma без Prisma Client: типобезпечні SQL-запити в Next.js
Дізнайтеся, як використовувати Prisma для роботи зі схемами, генерувати типи Kysely за допомогою prisma-kysely, обирати діалект Neon або pg та безпечно виконувати запити з коду сервера Next.js.
Джерела: **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 створюють версіоновані міграції, не змінюючи решту процесу роботи.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 у клієнтських компонентах, оскільки це дозволяє отримати секрет, доступний лише на сервері.
Щоденна робота
Як тільки все налаштовано, цикл виглядає так:
- Змінюйте моделі у файлі
db/schema.prisma. - Поново генеруйте типи та застосовуйте зміни за допомогою двох скриптів.
- Використовуйте
dbу серверських компонентах, обробниках маршрутів чи серверських діях.
Отриманий макет зберігає все, що стосується бази даних, під каталогом 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 до належних міграцій ще до того, як дані в базі даних почнуть мати значення.