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, а не в стандартном для Prismaprisma/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: достаточно передать пул соединений от 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. Этот компонент сервера выводит список пользователей, отбирая три столбца в порядке увеличения идентификатора:
// 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 к правильным миграциям до того, как данные в базе данных станут иметь реальное значение.