Чистый SQL, Prisma или Drizzle: выбор слоя базы данных для типобезопасности и контроля
В чём различия между чистым SQL, Prisma и Drizzle с точки зрения уровня контроля, безопасности типов и удобства работы для разработчиков в PostgreSQL, и как практически выбрать один из них для приложения на TypeScript.
Источники: Prisma vs Drizzle vs Raw SQL: What Every Backend Developer Should Understand Before Choosing an ORM
Большинство работ с бэкендом начинается с настройки API для получения ответов. Сначала настраиваются маршруты, добавляется один-два контроллера, внедряются механизмы аутентификации и некоторые промежуточные компоненты — на этом этапе всё выглядит довольно просто. Затем возникает более сложный вопрос: как именно приложению следует взаимодействовать с базой данных? Вдруг появляется множество терминов (чистый SQL, драйверы, ORM, инструменты построения запросов, Prisma, Drizzle) и сильных мнений во всех направлениях. Некоторые инженеры вообще не используют ORM; другие утверждают, что Prisma сэкономил им недели работы; третья группа выбирает Drizzle, поскольку он сохраняет SQL видимым, а не скрывает его. В следующих разделах тема рассматривается с самых основ, так что выбор между чистым SQL, Prisma и Drizzle для стека TypeScript и PostgreSQL становится вопросом инженерных решений, связанных с контролем и типобезопасностью, а не модой.
Как запрос доходит до базы данных
Прежде чем название любого инструмента становится понятным, полезно понимать, как запрос проходит через типичный бэкенд. Кто-то посещает ваш сайт и нажимает на элемент управления, например «показать мой профиль». Эта действие начинается в браузере, затем проходит через ваш API, доходит до слоя базы данных и, наконец, к самой базе данных, прежде чем ответ вернётся обратно.
Browser
|
vExpress API |
vDatabase Layer |
vPostgreSQL Database |
vResponse back to the user
Именно в среднем слое базы данных сосредотачивается вся путаница. Вашему приложению нужен механизм для взаимодействия с базой данных, и существует три распространённых способа: написание сырого SQL вручную, использование ORM вроде Prisma или использование инструмента построения запросов, такого как Drizzle. Каждый из следующих разделов рассматривает по очереди один из этих способов.
Написание SQL вручную
Самый прямой путь — это также самый старый: вы сами пишете SQL-запросы. Представьте простую таблицу пользователей с обычными столбцами.
users
id
name
email
password
Загрузка информации об одном пользователе происходит крайне просто.
SELECT *
FROM users
WHERE id = 1;
Поскольку база данных использует SQL в качестве своего основного языка, ничего не переводится при вводе или выводе данных. В Node.js с PostgreSQL стандартным способом напрямую отправки запросов является пакет pg; из него импортируется пул подключений.
import { Pool } from "pg";
Затем вы настраиваете пул с помощью переменной среды и выполняете параметризованный запрос к нему, считывая полученные строки.
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});const result = await pool.query(
"SELECT * FROM users WHERE id = $1",
[1]
);console.log(result.rows);
Ваш код отправляет SQL-запрос, PostgreSQL его выполняет, и результаты возвращаются сразу, без какой-либо дополнительной обработки посредине. Писание SQL самостоятельно даёт полный контроль над процессом. Например, сложные отчёты формируются напрямую: происходит объединение данных пользователей с их заказами и их группировка для подсчёта количества заказов у каждого пользователя.
SELECT
users.name,
COUNT(orders.id)
FROM users
JOIN orders
ON users.id = orders.user_id
GROUP BY users.name;
Для сложных аналитических запросов подобного рода рукописный SQL действительно трудно превзойти. Его написание также заставляет человека понимать объединение таблиц, индексы, планы выполнения и общие аспекты производительности, причем эти знания остаются ценными независимо от того, какой инструмент будет использоваться позже. Кроме того, между кодом и движком нет дополнительного слоя.
Недостатки проявляются, когда проект вырастает за пределы нескольких запросов. Представьте кодовую базу, в которой расположены сотни таких запросов, включая множество подобных поисков по электронной почте.
const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
[email]
);
Предположим теперь, что кто-то переименовал столбец, превратив email в email_address. Любой запрос, использующий этот столбец, будет неправильно работать. База данных сразу же фиксирует изменение, но ваш код на TypeScript об этом не узнает, пока что-то не сломается во время выполнения. Существует более тонкая версия той же проблемы. Допустим, вы берете первую строку.
const user = result.rows[0];
Затем вы читаете из него свойство с опечаткой.
console.log(user.emial);
Опечатка в слове emial не вызывает никаких ошибок в JavaScript. Вы обнаруживаете её только тогда, когда эта строка выполняется и приводит к сбою у пользователя. Именно такие проблемы стали причиной появления ORM.
Что такое ORM на самом деле
ORM — это аббревиатура от Object Relational Mapping, которая звучит более впечатляюще, чем на самом деле. По сути, речь идёт о мосте между вашим языком программирования и базой данных. Вместо того чтобы вручную писать подобные запросы,
SELECT *
FROM users
WHERE id=1;
Вы пишете что-то, близкое к вызову метода у объекта.
user.findUnique({
id: 1
})
ORM берет этот код и на фоне генерирует настоящий SQL-запрос, так что ваш код проходит через ORM к сгенерированному запросу, а затем к базе данных.
Your TypeScript code
|
vORM |
vSQL query |
vDatabase
Команды обращаются к ORM, потому что современные приложения нуждаются не просто в способе отправки запросов. Им требуется типобезопасность, реальные миграции, управление схемой, не зависящее от запоминания каждой таблицы, а также значительно меньше повторяющегося шаблонного кода. В современном мире TypeScript наиболее часто упоминаемыми названиями являются Prisma, Drizzle, TypeORM и Sequelize. Внимание здесь сосредоточено на чистом SQL, Prisma и Drizzle, поскольку именно эти три инструмента охватывают то, что действительно учитывают большинство современных серверных частей на TypeScript.
Знакомство с Prisma
Prisma является одним из наиболее обсуждаемых ORM в экосистеме TypeScript и часто первым упоминается в любых разговорах о современных серверных частях на Node.js. По сути, он позволяет приложению на TypeScript взаимодействовать с базой данных с помощью обычного TypeScript, а не со всеми местами, где используется SQL, причем он поддерживает PostgreSQL, MySQL, SQLite, SQL Server и MongoDB. Вы пишете код на TypeScript, а Prisma преобразует его в запросы.
TypeScript code
|
vPrisma Client |
vSQL queries |
vDatabase
Типичная структура приложений на Express, Prisma и PostgreSQL организована четко: запросы поступают через API, проходят через слой сервисов и достигают базы данных с помощью Prisma Client.
Client
|
vExpress API |
vService Layer |
vPrisma Client |
vPostgreSQL Database
Prisma Client — это тот компонент, который выполняет основную работу: он служит настоящим мостом между кодом вашего бэкенда и базой данных. Всё начинается с одного файла schema.prisma, в котором описывается вся структура базы данных в одном месте. Например, модель пользователя содержит описание четырех столбцов с указанием их типов.
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
}
Этот блок указывает Prisma на необходимость создания таблицы User с указанными столбцами, и Prisma точно понимает, что это означает. В то время как в сыром SQL для создания той же таблицы требуется явное указание.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
email TEXT UNIQUE,
password TEXT
);
Prisma нуждается лишь в приведённом выше блоке схемы и самостоятельно преобразует её в реальные изменения в базе данных с помощью миграций. Миграция по сути представляет собой запись истории изменений базы данных со временем. Предположим, что таблица пользователей изначально минимальна.
User
id
name
email
Через несколько месяцев вы хотите добавить номер телефона. Вместо того чтобы вручную изменять таблицу, вы выполняете одну команду.
npx prisma migrate dev --name add_phone_number
Prisma генерирует файл миграции для фиксации таких изменений, поэтому папка развивается от начальной миграции, связанной с настройкой, до миграции, добавляющей номер телефона; вся история остается в версионированном виде, а не остается только в памяти кого-либо. После определения схемы Prisma генерирует клиентский код, который используется в программе, например, для получения информации об одном пользователе по его идентификатору.
const user = await prisma.user.findUnique({
where:{
id:1
}
});
За кулисами — то, что незаметно превращается в обычный SQL.
SELECT *
FROM users
WHERE id=1;
Вам никогда не приходилось вручную писать SQL-запросы. Повседневные операции также интуитивно соответствуют определенным функциям: вставка данных превращается в вызов функции create, получение всех записей — в вызов функции find-many, обновление записи — в вызов функции update с использованием условия where, а удаление — в вызов функции delete. Главное преимущество Prisma — это безопасность типов. Если в вашей схеме определен объект User с полями id, name и email, то при выполнении запросов доступна настоящая автодополнение для каждого поля.
const user = await prisma.user.findMany();
user[0].email;
Если случайно написать user[0].emali, Prisma вместе с TypeScript обнаружат эту ошибку ещё до запуска приложения, что со временем позволяет существенно сократить время на отладку. Конечно, в реальных приложениях редко бывает всего одна таблица. Например, у пользователя может быть множество публикаций, поэтому существуют таблицы для пользователей и для публикаций, связанные между собой через идентификатор пользователя.
User
id
namePostid
title
userId
Prisma отражает эту связь непосредственно в схеме, как в модели пользователя,
model User {
id Int @id @default(autoincrement())
и модель поста, которая через связь указывает на своего пользователя.
name String posts Post[]
}model Post { id Int @id @default(autoincrement()) title String userId Int user User @relation(
fields:[userId],
references:[id]
)}
Загрузка пользователя вместе с его публикациями теперь представляет собой один читаемый запрос, включающий соответствующие записи.
const user = await prisma.user.findUnique({
where:{
id:1
},
include:{
posts:true
}
});
Это возвращает объект пользователя с вложенным массивом постов — именно такая структура требуется в коде приложения. Преимущества Prisma заключаются в том, насколько естественно ощущается процесс разработки, насколько высока уровень безопасности типов в проектах на TypeScript, насколько организованными становятся миграции, насколько качественной является документация, и насколько быстро вся команда может прочитать схему и понять структуру базы данных.
Это не лишено компромиссов. Действительно сложная отчетность может показаться неудобной по сравнению с ручным написанием SQL-запросов. Поскольку Prisma настолько эффективно скрывает SQL, разработчики, которые изучают только Prisma, иногда сталкиваются с трудностями при необходимости оптимизации на уровне базы данных. Кроме того, в пути запроса технически присутствует дополнительный этап — от приложения к Prisma, затем к SQL и, наконец, к базе данных, вместо более короткого пути от приложения к SQL, а затем к базе данных. Для большинства приложений этот дополнительный слой не является проблемой, но важно знать о его существовании. Prisma наилучшим образом подходит тогда, когда вы разрабатываете API на TypeScript, хотите быстрой разработки, в основном нуждаетесь в стандартных операциях создания, чтения, обновления и удаления данных, и очень цените пользовательский опыт разработчиков, поэтому многие стартапы и SaaS-продукты используют его.
Где подходит Drizzle
Drizzle — ещё один ORM, который действительно набрал популярность в TypeScript, и для его понимания необходимо сначала осознать, что Prisma и Drizzle основаны на совершенно разных философиях. Prisma стремится максимально избавить разработчиков от сложностей базы данных. Drizzle занимает противоположную позицию: он приближает разработчиков к SQL и просто добавляет на его основе механизмы безопасности TypeScript. Оба инструмента стремятся к одной цели — улучшить работу с базой данных — но используют для этого совершенно разные подходы. Drizzle — это лёгкий ORM на TypeScript для PostgreSQL, MySQL и SQLite, ориентированный на типобезопасность, высокую производительность, близость к SQL и простую архитектуру. Вы по-прежнему пишете код на TypeScript, который через Drizzle преобразуется в запрос и отправляется в базу данных.
TypeScript code
|
vDrizzle ORM |
vSQL query |
vDatabase
Размещение этих двух элементов рядом друг с другом явно подчеркивает философскую пропасть между ними. Получение всех пользователей с помощью Prisma происходит в формате, показанном ниже, причем SQL-запрос, который при этом генерируется, редко приходит в голову пользователю.
const user = await prisma.user.findMany();
Drizzle требует формата, который гораздо ближе к реальному SQL.
const users = await db
.select()
.from(usersTable);
Поведение на уровне базы данных можно практически напрямую прочитать в коде. Схемы также отличаются. Prisma хранит их в специальном файле schema.prisma, в то время как Drizzle объявляет схему на обычном TypeScript, сначала импортируя необходимые инструменты для создания столбцов.
import {
pgTable,
serial,
varchar
} from "drizzle-orm/pg-core";
и объявление таблицы с заданными столбцами.
export const users = pgTable("users", { id: serial("id").primaryKey(), name: varchar("name"), email: varchar("email")});
Структура вашей базы данных находится непосредственно внутри кодовой базы TypeScript, поэтому не требуется использование какого-либо дополнительного схематического диалекта. Повседневные операции напоминают SQL, но реализованы с использованием TypeScript: вставка данных осуществляется с помощью функции insert, передающей значения, чтение всех данных — с помощью запроса select от таблицы, а фильтрация добавляется с использованием условия where, сформированного с помощью оператора равенства. Люди предпочитают Drizzle по нескольким причинам. Он действительно легковесен и практически не содержит абстракций. Благодаря тому, что он остается близок к SQL, ваши навыки работы с SQL продолжают совершенствоваться, а не теряются из-за абстракций. Генерируемые им запросы, как правило, предсказуемы, поскольку между вашими намерениями и результатом минимум промежуточных элементов. Кроме того, он разработан с учетом современных сред выполнения, включая среды типа edge и serverless.
С другой стороны, если вы никогда раньше не изучали SQL, Drizzle может показаться более запутанным по сравнению с Prisma, который, как правило, более дружелюбен к новичкам. Для эффективного использования Drizzle действительно необходимо понимание объединений таблиц, связей, индексов и способа работы запросов, без возможности избегать какого-либо из этих аспектов. И хотя его экосистема, документация и сообщество солидны, они остаются меньше по размеру по сравнению с более развитой средой Prisma. Самый точный способ описать это сравнение: с Prisma база данных рассматривается как набор объектов, причем акцент делается на пользовательском опыте разработчика, тогда как с Drizzle база данных рассматривается скорее как сам SQL, и акцент смещается в сторону производительности и контроля.
Три подхода рядом друг с другом
Учитывая все три подхода, их архитектуры представляют собой спектр количества слоев, находящихся между приложением и базой данных.
Raw SQL: Application → SQL Query → Database
Prisma: Application → Prisma Client → SQL → Database
Drizzle: Application → Drizzle → SQL → Database
Рассмотрим простую операцию — поиск пользователя по электронной почте в базовой таблице пользователей. В чистом SQL запрос отправляется непосредственно с параметрами.
const result = await pool.query(
`
SELECT *
FROM users
WHERE email=$1
`,
["rahul@test.com"]
);
В Prisma вы вызываете операцию поиска по уникальному ключу.
const user =
await prisma.user.findUnique({
передача адреса электронной почты в условии where.
where:{
<email:"rahul@test.com>"
}});
В Drizzle вы создаёте фильтр, ограниченный условием равенства в столбце email.
const user =
await db
.select()
.from(users)
.where(
eq(
users.email,
"rahul@test.com"
)
);
В итоге все три инструмента возвращают одни и те же данные. Разница заключается в том, насколько сильный контроль вы хотите иметь, насколько вы готовы к абстракции и насколько хорошо вы уже знаете SQL. Распространенная ошибка начинающих — сосредотачиваться на том, какой инструмент работает быстрее, хотя в реальных проектах скорость редко является решающим фактором. Более важными вопросами являются то, с чем команда уже работала ранее, насколько на самом деле сложен проект, насколько глубоки знания участников в области баз данных и насколько сложными могут стать запросы. Для простого приложения типа SaaS обычно достаточно только Prisma, тогда как для приложений с большим объемом работы с базами данных начинают проявлять свою ценность Drizzle или использование чистого SQL в определенных местах.
Если кратко описать все нюансы: кривая обучения самая сложная при использовании чистого SQL, самая простая — с Prisma, а с Drizzle она умеренная. Контроль над SQL максимальный при использовании чистого SQL, высокий — с Drizzle, а минимальный — с Prisma. Безопасность типов слабая при использовании чистого SQL, но отличная как с Prisma, так и с Drizzle. Производительность чистого SQL и Drizzle отличная, а у Prisma — хорошая. Удобство работы для разработчика наибольшее с Prisma, хорошее — с Drizzle, а с чистым SQL — среднее. Для сложных запросов предпочтительнее чистый SQL и Drizzle, Prisma занимает промежуточное положение. Что касается простоты использования для начинающих, то Prisma лидирует, Drizzle находится посередине, а чистый SQL отстает.
Так что же выбрать?
Должен ли ORM всегда быть вашим стандартным инструментом? Честно говоря, нет. Как и любой другой инструмент, ORM подходит для некоторых задач, но не подходит для других, и ничто не подходит ко всем проектам. Чистый SQL находит своё применение тогда, когда требуется максимальный контроль, когда приложение действительно сильно зависит от базы данных, когда запросы становятся сложными или когда производительность просто не может снижаться. Это подходит для аналитических платформ, систем отчётности и финансовых приложений, а также для всего, что обрабатывает большие объёмы данных. Отчёт, в котором указывается, сколько каждый клиент купил за пять лет, а также средняя стоимость его заказа, естественно, проще сформировать непосредственно с помощью SQL, используя функции агрегации.
SELECT
customers.name,
COUNT(orders.id),
AVG(orders.amount)
и объединяя и группируя их для создания результата.
FROM customersJOIN ordersON customers.id = orders.customer_idGROUP BY customers.name;
Для подобной работы по-настоящему ничто не может заменить возможности SQL. Проблема в том, что использование вручную написанного SQL для всего в крупной базе кода быстро становится неудобным: возникают сотни файлов с запросами, разбросанными по разным местам, и необходимо самостоятельно контролировать безопасность ввода данных. Приоритетно-упорядоченный обзор производительности API Node.js показывает, что именно в базе данных, а не в фреймворке, заключаются настоящие преимущества и недостатки.
Prisma предпочтительнее при разработке бэкенда на TypeScript, когда основные операции — это стандартные создание, чтение, обновление и удаление данных, важна скорость разработки, и необходимо координировать работу нескольких разработчиков над одним кодовым базисом. Это относится к SaaS-продуктам, панелям управления, API магазинов и системам управления контентом, где повседневные задачи включают создание пользователей, обновление профилей, оформление заказов или получение данных с панели управления. Prisma действительно повышает производительность, превращая примитивные запросы в чистые и понятные вызовы, которые любой коллега может сразу осмыслить. Drizzle подходит тем, кто работает с TypeScript, уже хорошо знает SQL, ценит производительность и контроль и предпочитает легковесные решения вместо тяжелых. Он привлекает разработчиков, которые хотят иметь защиту от ошибок ORM, не теряя при этом тесной связи с базой данных.
На практике многие реальные проекты не придерживаются одного подхода. Стартап может разумно начать с прямого пути от Express через Prisma к PostgreSQL.
Express → Prisma → PostgreSQL
Это действительно разумный первый шаг. Позже, когда продукт развивается и появляется потребность в более сложном анализе или отчетности, команды часто просто добавляют сырые SQL-запросы в определенные части кода вместо того, чтобы полностью отказаться от Prisma, в результате чего приложение подключается к PostgreSQL как через Prisma, так и через вручную написанные запросы.
Application
├── Prisma
└── Raw SQL Queries
↓
PostgreSQL
Использование одного подхода везде не является обязательным правилом, и в большинстве реальных кодовых баз они смешиваются. Стартапы часто опираются на TypeScript, Prisma и PostgreSQL исключительно ради скорости. В новых проектах на TypeScript всё чаще используется Drizzle с его легким стилем, ориентированным на SQL. А более крупные системы часто сочетают ORM с необработанным SQL и специализированной оптимизацией базы данных, просто потому что разные части большой системы сталкиваются с разными проблемами.
Ошибки, которых стоит избегать
Повторяются некоторые ошибки. Первая — рассматривать ORM как базу данных. Prisma не является базой данных, как и Drizzle; это инструменты для работы с базами данных, будь то PostgreSQL, MySQL или MongoDB. Вторая ошибка — полностью игнорировать SQL и обращаться напрямую к Prisma. Сначала это кажется эффективным, но обычно это приводит к проблемам, когда медленные запросы становятся непонятными, объединения данных кажутся излишне сложными, а настройка становится по-настоящему трудной из-за отсутствия основы под абстракцией. ORM никогда не предназначался для замены SQL; он работает поверх него. Третья ошибка — стремление прежде всего к высоким показателям производительности. В реальном мире так же важна производительность разработчика, и сокращение времени выполнения запроса на пять миллисекунд за счёт увеличения времени разработки на три месяца редко бывает разумной заменой.
Путь обучения, который окупается
Начиная с нуля сегодня, разумный порядок действий — с основ SQL, включая таблицы, первичные ключи, внешние ключи, объединения, индексы и транзакции, с использованием такой системы, как PostgreSQL. Затем потратьте время на прямое взаимодействие с базой данных с помощью драйвера, например, пакета pg от Node, чтобы понять, как приложение общается с базой данных без каких-либо посредников. Далее переходите к Prisma и ознакомьтесь с схемами, миграциями, связями между объектами и системой типобезопасности. Наконец, попробуйте Drizzle, чтобы ощутить, как выглядит инструмент для построения запросов, ориентированный на SQL, а также какие есть компромиссы в плане производительности.
К чему это приведёт
Цель ORM никогда не заключалась в замене SQL. Он существует для улучшения работы разработчика, и ничего более важного в этом нет. Действительно сильный разработчик бэкенда определяется не знанием Prisma или Drizzle, а способностью понимать, как на самом деле работает база данных, умением писать SQL там, где это необходимо, осведомленностью о том, что упрощает ORM, и, что не менее важно, пониманием того, когда целесообразно использовать ORM, а когда разумнее писать SQL вручную. Как только появляется такая ясность, выбор между этими инструментами перестаёт быть угадыванием. Вы начинаете принимать решения на основе реальных потребностей проекта, а не на основе того, что сейчас в моде.
Связанные статьи
- Распространённые заблуждения относительно Node.js и баз данных, приводящие к сбоям в производственной среде: Узнайте, почему механизмы async/await, пулы подключений и ORM не обеспечивают автоматической защиты от ситуаций соревнования, исчерпания подключений или внедрения SQL-инъекций в приложениях на Node.js.
- Балансировка генерации CRUD-методов с Prisma и целенаправленный контроль маршрутов: Узнайте, как генерация CRUD-маршрутов с использованием схемы Prisma позволяет избавиться от повторяющегося шаблонного кода, сохраняя при этом принятие решений по надёжности, ограничениям и доступу прямо в коде приложения.