Галоўная / Артыкулы / Дрізл чы прызма? Пераканаўцеся з типам спаювання і запісаным SQL-кодам пры выборе.

Дрізл чы прызма? Пераканаўцеся з типам спаювання і запісаным SQL-кодам пры выборе.

Створыце тыя ж таблицы для корыстнікаў і рачунакоў у Drizzle і Prisma, парыяце тыпы рэзультатаў з’еднання і запісаны SQL-код, а таксама выявіце налаштаванні драйвераў, якія ператвараюць сумы на строкі.

2461 слоў

Дыялогі ў абласці ORM часта центруюцца навакола статыстык завантажэння і слоганаў канферэнцый, але практычныя запытанні ў рэальных умовах працы ёсць значна простэйшыя: калі вы спаўнюеце звязак межу корыстнікам і яго рахункамі-фактурамі, канкрэтны тип мае атрыбут total, і чы можна прачытаць SQL-запыт, які його створыў? У гэтым кяле ствараюцца тыя ж два таблицы ў Drizzle і Prisma, выпалююцца адпаведна адны запыты на вставку дадзеных і спаўненне звязку, пасля чаго поручваюцься выведзеныя типы дадзеных у TypeScript, запісаныя SQL-запыты, рэзультаты міграцый і поведанне системы пад час виконання коду на TypeScript у самай сэрвісной супераспалатцы Node. Вы отрымаеце короткі, можна павтарыць эксперымент, які дае адказ на запытанні ў абласці ORM для вашай сэрвісной базы, а не на падставе чужых тестаў. Чыба ўжо для шырэйшага крэатыву пры выборе, які таксама врачоўвае чысты SQL, адзірніце як выбраць шар базы дадзеных між чыстым SQL, Prisma і Drizzle.

Для чаго оптымізуецца кожны інструмент

Этыя два бібліятэкі даюць разныя абяцанкі. Drizzle прысвячаецца стварэнню коду запитоў, які нагадвае SQL, напісаны на TypeScript, без аддзельнага процесу двігу запитоў, і мае дизайн, які падходзіць для рэальных часоў на перыметры. Prisma прысвячаецца падходу, у яком спачатку ствараецца схема, з аддзельным файлам schema.prisma і генераваным кліентам; ў яе няўздоўж часу выходзіць новыя версіі, якія пераводзяць ўжо ўжо двіг запитоў з Rust на TypeScript. У момент напісання гэты пераход яшчэ трываў, таму пераканайцеся ў актуальных прыметках да выходу Prisma, канкрэтна які двіг викорыстоўваецца у вашай версіі.

Популярнасць таксама мае два бакоў: Prisma яшчэ застаецца лідерам па колькасці інсталляцый, тады як Drizzle домінуе ў размовах пра рост. Нічыга з гэтага не паведамляе вас пра ваш участак. Два крэтарыя, якія викорыстоўваюцца нижэй, ўмышлена є вузкімі і практычнымі: чы рэзультат total прыходзіць у вастоўце number, і чы SQL-код у лагах — гэта тое, што вы моглі б з адчуванням довер'я падставіць у psql пад час інцидэту.

Сценарый — маленькія програмы для выставлення рахункоў, дзе стораніца /invoices должна адобразыць загальную суму. Часцякі системы должны надаць гэтай суме паводлі, і самэлькі тут і начынаецца парабян.

Тыя ж два табелі, два разы

Створыце два адзеленыя папкі проекта на той самы экземпляр PostgreSQL і дайце кожнай з іх сваё імя схемы. Аднароўненне табеліў между двума ORM-системамі прыводзіць да дублювання рэкордаў, якія выглядзяюць як даны пра выдатковасць, але на самай справе ўсё ж такія баги.

У Drizzle схема знаходзіцца ў src/schema.ts у вигляде звычнага TypeScript. Заўважкайце, што назвы столбцаў прызначаюцца явна у формате snake_case (user_id), тады як атрыбуты — у формате camelCase (userId), а звязаны ключ ёсць це пасыланне на функцію users.id:

import { integer, pgTable, uuid, varchar } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: uuid("id").primaryKey().defaultRandom(),
  email: varchar("email", { length: 255 }).notNull().unique(),
});

export const invoices = pgTable("invoices", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull().references(() => users.id),
  total: integer("total").notNull(),
});

У Prisma той самы модэль знаходзіцца ў prisma/schema.prisma. Связь прызначаецца з обох бакоў: User мае массив invoices, а Invoice мае скалярны атрыбут userId плюс атрыбут @relation, які з’ѐднае іх:

model User {
  id       String    @id @default(uuid())
  email    String    @unique
  invoices Invoice[]
}

model Invoice {
  id     String @id @default(uuid())
  userId String
  total  Int
  user   User   @relation(fields: [userId], references: [id])
}

Тепер сама важлівая запытка: адзначыць інвойсы пользователя па электронной пошцы. Drizzle выражае яе як явны внутрашні з’ѐднанне з клазузай where, тады як Prisma прасіць адзначыць самога пользователя і сказаць „адзначыць інвойсы“:

// drizzle
const rows = await db
  .select()
  .from(invoices)
  .innerJoin(users, eq(invoices.userId, users.id))
  .where(eq(users.email, email));

// prisma
const user = await prisma.user.findUnique({
  where: { email },
  include: { invoices: true },
});

Тыпы рэзультата адпраўляюцца да гэтых двух ментальных модэлей. Drizzle вяртае рядкі, якія апошнююцца па формату з’еднання, калі ў кожным рядку є ключ users і ключ invoices. Prisma вяртае User & { invoices: Invoice[] } — глыбока аб’ектна структура. Оба падходы ўсуненне правільныя. Формат Drizzle адпраўляецца да SQL; формат Prisma — да сторніцы, яую вы збіраецеся атрыбутаваць.

Калі ўвымкнута логаванне запытак, разлік застаецца. Рэзультат Drizzle — цэна з’еднання, яю развівач можа прымусова прачытаць. Рэзультат Prisma таксама ўсуненне практычны, але цэ гэта SQL, які не хацелася бы рэдагаваць вручную.

Для такага маленькага схемы міграцыі праходзілі без проблем. Команда drizzle-kit generate стварыла файлы SQL, якія можна зафіксаваць; каманда prisma migrate стварыла сае власную історыю міграцый, якая таксама можа быць зафіксаваная. Жадна з адзінакоў не мела проблем з двумя тэбламі, а схема такога розмеру не можа паказаць сложнейшыя прыклады міграцый, таму не трэба робіць жадных вывароў.

Відтворэнне лабараторныя суперблоку ў локальных умовах

Установіце кожную з адзінакоў у сваёй сабе папцы. Drizzle патрабуе ORM, драйвер (тут postgres) і drizzle-kit для міграцый; Prisma патрабуе CLI і кліента, а таксама prisma init для стварэння файлу схемы:

pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit

pnpm add prisma @prisma/client
pnpm exec prisma init

У кожнай папцы дадзіце адного пользователя і два рахунку-фактуры, адзін раз выконайце спраўу з’едынэння, і выведзіце значэнне total першай рахунку-фактуры разам з яе типам пад час выконання. Зверніце увагу на разныя шляхі доступу: rows[0].invoices.total для рэядкаў, з’едыненых пры викорыстоўванні Drizzle, і user.invoices[0].total для вярнутага звёрнутага об’екта пры викорыстоўванні Prisma.

console.log(rows[0]?.invoices.total, typeof rows[0]?.invoices.total);
console.log(user?.invoices[0]?.total, typeof user?.invoices[0]?.total);

Якщо адзін інструмент паказвае string, а другі — number, прычына паўтараецца гэтая: часта ёю являецца карэткаванне типоў драйверам базы дадзенаў, а не філасофія ORM. Драйверы PostgreSQL часта вяртаюць столбцы bigint і numeric у вастоўе формате, каб утримаць точнасць дадзеных, тады как столбцы integer вяртаюцца як числы. Такім чынам, сума у вастоўе формате прыводзіць да таго, што "1200" + 50 на рахунку стае "120050". Перад выборам бібліятэкі запісаце рэзультат typeof.

Запуск файлу запытання з натыўным TypeScript

Далей пераканайцеся, чы роўнамерна код запускаецца пад вбудованым карэткаваннем типоў у Node, яке выконвае файлы .ts, адмаўляючы анотаціі типоў, без неабходнасці отрымання аддзельнай версіі:

node src/query.ts

Модуль Drizzle, які складаецца толькі з функцыяў і анотацый типаў, працаваў без проблем. Кліент Prisma, створаны ў падкаталогу node_modules, таксама працаваў, калі яго вызывалі з маленькага обгортка. Проблема выйшла з файлам, які імпортаваў энумы Prisma у старым стылі. Декларацыі enum у TypeScript — гэта не проста типы; яны компілююцца у об’екты часу выканання, і режым strip-only у Node не можа яны старыць, таму выкананне збіваецца. Гэта не дыфект Prisma, а сутнасць створанага коду часу выканання. Якщо ваша версія Prisma выкарыстоўвае новейшы двыжок і генератор, заснованыя на TypeScript, пераканайцеся, што самэ працюе prisma generate, перш чым припускать, што гэта яшчэ актуальна, і задаце конкрэтную версію, яку вы теставалі.

Увёмкненне логавання запытак

Адгадванне SQL-запыткаў — гэта прычына таго, чаму інцыдэты затрымляюцца. Обе бібліятекі можаўця зьвярнуць лог кожнай запытцы: Drizzle чераз опцыю logger, а Prisma черз массив log на кліянцы:

const db = drizzle(client, { logger: true });
const prisma = new PrismaClient({ log: ["query"] });

Пазначьце два запісаныя SQL-тэксты падле двух рэзультатаў typeof total. Гэтыя чатыры лініі — гэта весь набор дадзеных, якія патрэбны для гэтага эксперымента.

Калі кожны інструмент коштае вас

Компрэсіі пазначыліся ў пяці аспектах.

Тыпы. Функцыя include Prisma стварыла самэ тую структуру, якой патрабавала стораніца /invoices. Функцыя join Drizzle стварыла самэ тую структуру, якая патрэбна для дыбаггінгу прычыны, чаму сума падвоўвалася. Обе функцыі є корыстнымі у разных ситуацыях, і гэта ўзгоджваецца з практыкай выбіраць адну для кожной базы дадзеных, а не выкананняць обея проты разных табэляў.

Візуабельна частка SQL. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват. Калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват, калікват.

Этап генеравання. Prisma выкалічвае prisma generate пасля кожных змян схемы; Drizzle выкалічвае, каб schema.ts заставалася тачным. Этап генеравання лёгка адзначыць, і калі кліентская версія адстае ад схемы, гэта вызвалівае плутанне. Нехай CI збяжыцца, калі генераванне будзе праўдападзены.

Часы выканання на краю сеті. Спрыянне Drizzle для роботы на краю сеті — гэта справжнія прыемнасць, але гэта мае значэнне толькі тады, калі вы размешчаеце прыладу на такім часе выканання. Процес Node, які работае празаўседы ў VPS разам з PostgreSQL, нічога не атрымлець ад гэтага, таму не дазвольце, каб гэты аргумент вялі за сабой выбор для прылады, якая работае на серверы.

Грані пакетаў. Жаданы ORM не патрабуецца ў кліентскай складовай. Якщо хоць адны з іх імпортуецца ў модуль "use client", напрыклад, для фільтрацыі інтэрактыўной таблыцы, грань кліента будзе пазначана занадта высока, і драйвер базы дадзеных патрапіць у браузер. Старанне пазначэння грані use client правільна раскрывае, як гэта паспрабаваць выправіць.

Пераказ з перыякшэнняамі

Дальнейшая разбівка расходаў:

  • Час. Фракцыя Drizzle была наследкам формату з’ѐднання: rows[0].invoices.total або rows[0].total, залежна ад таго, як была напісана задача выбору. Фракцыя Prisma полягала ў неабходнасці перагенеравання пасля кожной змены схемы.
  • Працэўнае падзея. Ёжы разам, ёжы з’едыненыя — у обох выпадкаў вяртаюцца два рачынання. Две табелі ніколі не зможуць стаць паводлай.
  • Enums. Enums, створаныя Prisma, ўособліваюць сабе як значэння пад час выканання. Выконаванне гэтых файлаў праз простае адзьемленьне типаў у Node не ўдаёцца; трэба альбо скомпіляваць гэты пакет, альбо утримацца ад безпосередньага запуску створаных кансультацый.
  • Залучэнне. Кліент Prisma — это продукт з саеўным генератаром і двіжкам; табелі Drizzle — це просты TypeScript. Якщо пасля года застацца з адним з іх, даведзецца перапісваць шар запытанняў, а не проста змяніць настройкі. Прыключыце гэта ў RFC, пакуль хтось не запісае „мы завжды можамы перайсці пазней“.
  • Выбір і чаго не робіць

    Выберыце Drizzle, калі хочаце, каб SQL быў видны ў пераглядзе коду, а команда вядома працавала з споўненням запытам. Зберагуйце схему ў schema.ts і пераканаўцеся, што хтось у командзе ведае, як чытаць innerJoin.

    Выберыце Prisma, калі прычыны работы команды базуюцца на schema.prisma і include. Заложыце бюджет на крок генеравання ў CI і не дазволіце запуску пайплайну, якщо ён не быў адбыты.

    Незалежна ад выбору, утримваюцеся ад гэтага:

    • Адбывання запытоў обох ORM-аў на тых сабе вырабочых таблицах „для паўтарнага порэвання“. Так можа выйсці, што одна і тая ж сума будзе запісана два разы, і хтось будзе витрачаць дзень на падтрымку рэквізыцый у адпаведнасці з даннемі банку.
    • Выбор на адзеўнічных завантажэннях. Выберыце той ORM, чыяе рэзультаты споўнення запытам можна быстра прачытаць пад тэншчу.
  • Для зручнасці кліент даблэ-базы дадзеных імпортуецца ў Server Action, а таксама ў Client Component. Сама гэта зручнасць і стварыла ситуацыю, калі кліентскі модуль пачаў выкладзаць драйвер.
  • Аб’ём у формате строкі, які на самай працоў быў проблемай драйвера

    Рэалістычны прыклад абярвання паказвае, чаму важліва перацэнка з выкарыстоўванням typeof. Команда стварыла модэлі тых сабе двух таблиц у обох інструментах, паў’язала аднарозовага корыстніка з двумя рахункамі-фактурамі і запісала тип значэння total: у обох случаях — number. Чэрез тыдзень быў запрацоўаны іншы драйвер, які перакладае числовы столбец у формат string, і звіт пачаў склеюваць значэння замест таго, каб іх аддаць, чым удвое збільшыліся показаны цифры.

    Парадоксальным рашэнням ў такіх ситуацыях яўляецца абгортаванне Number(total) навакола кожнага месца вызову. Гэта толькі закрывае проблему з відображанням дадзеных, але не рашае ёё, і наступны столбець з тым жа прынцыпам таксама застанется нерашаным. Надзеяным рашэнням є логаванне SQL-запытов і типаў рэзультаата адно раз пры кожнай бібліятэце, фіксацыя версіі драйвера і запэчатаванне таго, каб два ORM не моглі пісаць у тыя ж таблицы ў працоўным серавере.

    Работа з enum-амі таксама выконвачыцца за той жа принцып: генераваныя enum-ы ўособлівае код, таму трэба скомпіляваць гэты пакет і запускать выходны код з каталога dist/, а не выплываць генераваны TypeScript безпосередна. І незалежна ад таго, якая бібліятэца будзе выбраная, трэба запісаць рашэнне і прычыны ў файле README, каб ніхто пазней не дадаў другую бібліятэцу проста для прыкантролю.

    Запіс абставінакоў среды пры парабяранні

    Такі рэзультаты маюць значэнне толькі ў сувязі з версіямі, якія іх стварылі. У даным случае базовая среда складалася з Node 24, TypeScript 7 і Next.js 16.3, на якіх запускаўся невялікі прыемнік для стварэння рахункоў-фактур з чатырма маршрутамі. У репазітарыі трэба зберагчы файл notes/lab.md і спачатку зафіксаваць тры версіі:

    node -v
    pnpm exec tsc -v
    pnpm exec next --version
    

    Запішыце іх у верхней часты нотатак. Якщо значныя версіі не збігаюцца з тымі, якія прыпускае інструкцыя, зупініцеся і узгодзіце ўсё, перш чым запускаць што-небудзь інше, адтак як пазнейшыя каманды могу вас абмануць болей тонкімі спосабамі.

    Потым запускайце сервер разработчика і пераходзіце па маршрутам:

    pnpm exec next dev
    

    Адвяжыцеся за адресамі /, /invoices, /invoices/1, /settings, а пасля знову за /invoices, увімкнуўшы опцыю „Preserve log“ у інструментах разработчика. Зафіксавайце поле фільтра разам з URL; гэта поўнай меры можа стаць неабходным доказам пазней.

    Потым запустіце перакладчык типаў і выведзіце його код завершэння:

    pnpm exec tsc --noEmit --pretty false
    echo $?
    

    Код завершэння 0 не є якім-небудзь прыемлівым рэзультатам, а толькі параднем для пераходу да перакрыцэнняў пад час выканання. Пасля этага запустіце каманды з вышэйшага раздзелу лабораторных заведамаў на свайму сэрвере, а не пакладаючыся на гэтыя рэзультаты; апаратна частка, напор на памяць і ўсё, што робіце браузер, можа змяніць выкарыстоўвання памяці, час перакрыцэння типаў і тымчасовыя показнікі загадоў больш, чым могла бы зробіць незначная версія фрэймворку.

    Таксама корыстна ў записках залічваць адзін-радковы запіс пра «неудачную падтрымку», у формате «прабавана X, але ўсё равна бачыцца Y». Такі запіс ператварае файл у адкрывачны лабораторны звіт, а не проста у інформацыйны матэрыял, і ён являецца найкорыстнейшым элементам для колегі, які будзе продаваць расследаванне.

    Памылкі, якіх варта утрымацца

    Тры паслупнія помылкі, якія выступаюць у такім сораканні, часта павтараюцца:

    • Адмініструванне або выконанне обох інструментаў міграцыі проты той самай базы дадзеных для ўпораўнэння, што залучае два історыкі міграцый і адну табелю пад двума назвамі. Яедыны чысты спосаб рэкаверыяціі — це вярненне з резервной копіі.
    • Імпорт ствароўных прыкладаў Prisma у файл, які пасля таго праходзіць процэс адключэння типаў у Node, што не вялікае з вышэйзгаданых прычын. Краща скомпіляваць гэты пакет.
    • Оцэнка бібліятэк па колькасці завантажэнняў, якая не мае нічынага на адзін тип вашай з’еднання.

    Спіс пераконтрацоў пры дадаванні ORM

    • Адзін ORM на адную базу дадзеных.
    • Запіс SQL-кода для ключовай з’еднанняя прынеймна адной раз.
    • Запіс значэнняя typeof для столбцоў з данымі грошаў прынеймна адной раз, і зноў пасля кожнага апдэйта драйвера.
    • Скомпіляваны выходны файл, які мае прыклады, ніколі не выкананы праз процэс адключэння типаў.
    • У файле README павінны быць указаны выбраная бібліятэка і прычына ўжыванняя.

    Заключанне

    Два таблеты не ўтвараюць схему для практычнага викорыстання, і ў гэтай лабараторный роботы не адбывалася перацэнка тысяч споўнаў чы не выкладчыцтва на серверах на перыметры. Аднак гэта паказвае, што ключовыя разлікі є конкрэтнымі і можна пераканацца ў іх за аднаго дня: форма рэзультата споўна, чытымасць логаванага SQL-кода, вартасць крока генеравання данных, а таксама тое, чы рыхтар выдае вам числы чы строкі. Выберыце адну бібліятэку на кожную базу дадзеных, запісайце прычыны і зробіце перакананне typeof total рутынальным пасля кожнай змены рыхтара. Калі два інструменты міграцыі керуюць адной базой дадзеных, гэта заканчываецца ўскладненнямі пад час вярнэння да первачных дадзеных, таму трэба трывожна ставіцца да такіх эксперыментаў у системах, якія карыстуюцца рэальнымі грошамі.