Чистий SQL, Prisma чи Drizzle: вибір шару бази даних для безпеки типів та контролю
Яким чином raw 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 з ідентифікатором, ім’ям та електронною поштою, то під час запитів ви отримуєте справжнє автодоповнення для кожного поля.
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: вставка даних — це виклик функції вставки з передачею значень, читання всіх даних — це запит 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 ви створюєте вибірку, обмежену рівністю у стовпці електронної пошти.
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 на основі схеми дозволяє усунути повторюваний шаблонний код, зберігаючи при цьому рішення щодо довіри, обмежень та доступу в коді додатку.