Deszcz czy Prisma? Sprawdź typ połączenia oraz zapisany SQL przed podjęciem decyzji.
Stwórz modele tych samych tabel użytkowników i faktur w Drizzle oraz Prisma, porównaj typy wyników łączenia oraz zapisany SQL, a także wykryj mapowania sterowników, które zamieniają sumy na łańcuchy tekstowe.
Debaty na temat ORM zwykle toczą się wokół wykresów pobierania danych i hasłów konferencyjnych, jednak pytanie istotne w środowisku produkcyjnym jest znacznie prostsze: gdy łączysz użytkownika z jego fakturami, jaki typ ma total, i czy możesz odczytać SQL, który go wygenerował? Ten przewodnik tworzy te same dwie tabele w Drizzle i Prisma, wykonywa w każdym z nich jedną operację wprowadzania danych i jedno połączenie, a następnie porównuje typy TypeScript wywnioskowane przez narzędzia, zapisane zapytania, wyniki migracji oraz zachowanie w czasie wykonywania kodu w natywnym środowisku TypeScript Node’a. Otrzymasz krótkie, powtarzalne ćwiczenie, które odpowiada na pytanie dotyczące ORM dla twojego własnego kodu, a nie na podstawie benchmarków kogoś innego. Aby uzyskać szerszą ramę decyzyjną, która uwzględnia również surowy SQL, zapoznaj się z wskazówkami dotyczącymi wyboru warstwy bazodanowej pomiędzy surowym SQL, Prismą i Drizzle.
Dla czego optymalizuje się każde narzędzie
Dwie te biblioteki dają różne obietnice. Drizzle oferuje kod zapytań przypominający SQL napisany w TypeScript, bez oddzielnego procesu silnika zapytań oraz projekt dostosowany do środowisk edge. Prisma zapewnia podejście oparte na schemacie, wykorzystujące specjalny plik schema.prisma oraz generowany klient; w najnowszych wersjach silnik zapytań przenosi się od Rustu ku TypeScriptowi. W momencie pisania tego tekstu ta migracja wciąż trwała, dlatego sprawdź aktualne notatki wydawnicze Prismy, aby dowiedzieć się, jaki silnik jest używany w danej wersji.
Popularność też ma dwa oblicza: Prisma wciąż przewodzi pod względem liczby zainstalowań, natomiast Drizzle dominuje w dyskusjach na temat wzrostu. Nic z tego nie mówi nic o Twoim dołączeniu. Dwa kryteria użyte poniżej są celowo wąskie i praktyczne: to, czy total przychodzi w formie number, oraz to, czy SQL z logu to coś, co chętnie wkleiłbyś do psql podczas incydentu.
Sytuacja dotyczy małej aplikacji do wystawiania faktur, w której strona /invoices musi wyświetlić łączną kwotę. Coś w architekturze musi nadać tej kwocie odpowiedni typ, i właśnie tutaj zaczyna się porównanie.
Dwie te same tabele, dwa razy
Utwórz dwie oddzielne foldery projektów wobec tej samej instancji PostgreSQL i nadaj każdemu własną nazwę schematu. Dzielenie się tablicami między dwoma ORM prowadzi do powstania podwójnie zapisanych wierszy, które wyglądają jak dane dotyczące wydajności, ale w rzeczywistości są błędami.
W Drizzle schemat znajduje się w pliku src/schema.ts jako zwykły kod TypeScript. Zauważ, że nazwy kolumn są deklarowane wyraźnie w formacie snake_case (user_id), podczas gdy właściwość ma format camelCase (userId), a klucz obcy to odwołanie do funkcji 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(),
});
W Prisma ten sam model znajduje się w pliku prisma/schema.prisma. Związek jest deklarowany po obu stronach: User ma tablicę invoices, a Invoice zawiera wartość skalarową userId wraz z tagiem @relation, który łączy je ze sobą:
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])
}
A teraz zapytanie, które jest istotne: pobranie faktur użytkownika na podstawie adresu e-mail. Drizzle realizuje to poprzez wyraźne połączenie wewnętrzne z klauzulą where, natomiast Prisma prosi o dostarczenie użytkownika i dodaje instrukcję „włącz faktury”:
// 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 },
});
Rodzaje wyników odzwierciedlają te dwa modele myślowe. Drizzle zwraca wiersze w formie połączenia tabel, gdzie każdy wiersz zawiera klucz users oraz klucz invoices. Prisma zwraca User & { invoices: Invoice[] }, czyli zagnieżdżony obiekt. Obie metody są poprawne. Struktura danych w Drizzle odpowiada SQL, natomiast struktura w Prismie odpowiada stronie, którą zamierzasz wyświetlić.
Gdy jest włączone logowanie zapytań, różnica utrzymuje się. Wynik generowany przez Drizzle to połączenie tabel, które programista może bezpośrednio odczytać. Wynik Prismy jest w pełni użyteczny, ale stanowi SQL wygenerowany automatycznie, którego nie chciałoby się edytować ręcznie.
Migracje przebiegły bez problemów przy tak małym schemacie. drizzle-kit generate wygenerował pliki SQL, które można zapisać; prisma migrate stworzył własną historię migracji, którą również można zapisać. Żaden z tych narzędzi nie miał trudności z dwiema tabelami, a schemat takiej wielkości nie pozwala na wykrycie trudniejszych przypadków migracji, więc nie należy wyciągać z tego żadnych wniosków.
Odtworzenie laboratorium lokalnie
Zainstaluj każdy zestaw narzędzi w osobnej folderze. Drizzle wymaga ORM, sterownika (tutaj postgres) oraz drizzle-kit do przeprowadzania migracji; Prisma wymaga interfejsu wiersza poleceń oraz klienta, a następnie prisma init do utworzenia szkieletu pliku schematu:
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit
pnpm add prisma @prisma/client
pnpm exec prisma init
W każdej folderze należy dodać jednego użytkownika i dwie faktury, wykonać operację połączenia raz oraz wyświetlić wartość total pierwszej faktury wraz z jej typem w czasie wykonywania. Zwróć uwagę na różne ścieżki dostępu: rows[0].invoices.total w przypadku rzędów połączonych przez Drizzle, natomiast user.invoices[0].total w przypadku zagnieżdżonego obiektu w Prismie.
console.log(rows[0]?.invoices.total, typeof rows[0]?.invoices.total);
console.log(user?.invoices[0]?.total, typeof user?.invoices[0]?.total);
Jeśli jedno narzędzie raportuje typ string, a drugie number, przyczyną jest niemal zawsze mapowanie typów dostawcy bazy danych, a nie filozofia ORM. Dostawcy PostgreSQL często zwracają kolumny typu bigint i numeric jako łańcuchy, aby uniknąć utraty precyzji liczby w JavaScript, podczas gdy zwykłe kolumny typu integer wracają jako liczby. Suma w formie łańcucha to sytuacja, gdy "1200" + 50 potajemnie zamienia się na "120050" w fakturze. Zapisz wynik typeof przed wyborem biblioteki.
Uruchamianie pliku z zapytaniem za pomocą natywnego TypeScript
Następnie sprawdź, czy kod działa bezpośrednio przy użyciu wbudowanego w Node mechanizmu usuwania informacji typowych, który uruchamia pliki .ts, usuwając adnotacje typowe bez konieczności osobnego procesu budowania aplikacji:
node src/query.ts
Moduł Drizzle składający się wyłącznie z funkcji i adnotacji typów działał bez problemów. Klient Prisma wygenerowany do katalogu node_modules również funkcjonował po wywołaniu z małego wrappera. Problem pojawił się w przypadku pliku, który importował enumy wygenerowane przez Prisma w starszym stylu. Deklaracje enum w TypeScript to nie tylko typy; są one kompilowane do obiektów w czasie wykonywania, a tryb strip-only w Node nie może ich usunąć, przez co wykonywanie programu zawodzi. To nie jest wada Prisma, lecz cecha kodu generowanego do czasu wykonywania. Jeśli twoja wersja Prisma używa nowszego silnika i generatora opartych na TypeScript, sprawdź, co faktycznie wytwarza polecenie prisma generate, zanim założysz, że to nadal ma zastosowanie, oraz określ dokładną wersję, którą testowałeś.
Włączanie logowania zapytań
Domysły dotyczące SQL powodują, że incydenty się przedłużają. Obie biblioteki mogą rejestrować każde zapytanie – Drizzle za pomocą opcji logger, a Prisma poprzez tablicę log w klienta:
const db = drizzle(client, { logger: true });
const prisma = new PrismaClient({ log: ["query"] });
Umieść obie zapisane strony SQL obok dwóch wyników typeof total. Te cztery linie stanowią cały zestaw danych potrzebny w tym laboratorium.
Koszty posługiwania się każdym narzędziem
Kompromisy pojawiły się w pięciu obszarach.
Typy danych. Funkcja include w Prismie dostarczyła dokładnie taką strukturę, jakiej wymagała strona /invoices. Operacja łączenia w Drizzle dała dokładnie taki wynik, jakiego potrzebujesz podczas debugowania sytuacji, gdy suma się podwoiła. Obie metody są przydatne w różnych sytuacjach, co jest argumentem za wyborem jednej biblioteki na każdą bazę danych, a nie za używaniem obu do obsługi tych samych tabel.
Gdy suma wygląda niepoprawnie, dzięki czytelnej zapytaniu log Drizzle szybciej rozstrzyga ten problem. Gdy nowy członek zespołu musi dodać pole, plik schematu Prismy jest szybszym rozwiązaniem. To różne sytuacje, w których zwycięzca jest inny.
Prisma wymaga wywołania prisma generate po każdej zmianie schematu; Drizzle wymaga, aby plik schema.ts pozostawał dokładny. Krok generowania łatwo przeoczyć w środowisku CI, a wersja klienta opóźniona o jedną wersję względem schematu powoduje mylące błędy. Niech CI zwróci błąd, jeśli pominięto krok generowania.
Zdolność Drizzle do pracy w środowiskach Edge to rzeczywiście mocny atut, ale ma znaczenie tylko wtedy, gdy używasz takiego środowiska do rozgłoszenia aplikacji. Proces Node działający obok PostgreSQL na VPS nie czerpie z tego żadnych korzyści, więc nie pozwól, by ten argument decydował o aplikacji hostowanej na serwerze.
Granice pakietów. Żaden z ORM nie powinien znajdować się w komponencie klienckim. Jeśli któryś z nich zostanie zaimportowany do modułu "use client", na przykład w filtr tabeli interaktywnej, granica kliencka zostanie ustawiona zbyt wysoko, a sterownik bazy danych trafi do przeglądarki. Artykuł na temat poprawnego ustawiania granicy use client opisuje, jak to naprawić.
Rachunek w formie wykazu
Dalszy podział kosztów:
- Czas. Opóźnienia spowodowane przez Drizzle wynikały z konstrukcji połączenia:
rows[0].invoices.totallubrows[0].total, w zależności od sposobu napisania zapytania. Opóźnienia w Prismie wynikały z konieczności ponownego generowania danych po każdej modyfikacji schematu.
Wybór i co unikać
Wybierz Drizzle, gdy chcesz, aby SQL był widoczny podczas przeglądania kodu, a zespół już myśli w kategoriach połączeń. Zachowaj schemat w pliku schema.ts i upewnij się, że ktoś w zespole potrafi swobodnie czytać kod innerJoin.
Wybierz Prisma, gdy nawyki zespołu opierają się na pliku schema.prisma oraz funkcji include. Przygotuj budżet na krok generowania w procesie CI i spraw, aby pipeline nie przeszedł dalej, jeśli ten krok nie został wykonywany.
Niezależnie od wyboru unikaj następujących błędów:
- Wykonywania obu ORM na tych samych tabelach produkcyjnych „w celu porównania”. W ten sposób wartości mogą zostać zapisane dwukrotnie, a ktoś może spędzić dzień na porównywaniu faktur z danymi bankowymi.
- Decydowania na podstawie tygodniowych pobieranych danych. Wybierz taką opcję, której wynik połączenia możesz szybko odczytać pod presją.
Całkowita wartość typu string, która w rzeczywistości była problemem ze sterownikiem
Rzeczywisty błąd pokazuje, dlaczego sprawdzanie typu za pomocą typeof jest istotne. Zespół modeluje te same dwie tabele w obu narzędziach, łączy użytkownika z dwoma fakturami i rejestruje typ wartości total: w obu przypadkach number. Tydzień później wprowadzany jest inny sterownik, który mapuje kolumnę liczbową na typ string, w wyniku czego raport zaczyna łączyć wartości zamiast je dodawać, co podwaja pokazywane liczby.
Kuszącym rozwiązaniem jest otaczanie Number(total) wokół każdego miejsca wywołania. To ukrywa problem, zamiast go naprawić, a kolejna kolumna z tym samym błędem przejdzie niezauważona. Trwałe rozwiązanie polega na logowaniu zapytań SQL i typu wyniku raz na bibliotekę, ustaleniu konkretnej wersji sterownika oraz uniemożliwieniu dwóm ORM-om zapisywania danych do tych samych tabel produkcyjnych.
Obsługa typów Enum opiera się na tej samej logice: generowane typy Enum to kod uruchamiany w czasie wykonywania, więc należy skompilować ten pakiet i uruchomić pliki z katalogu dist/, zamiast bezpośrednio wykonywać generowany kod TypeScript. Niezależnie od tego, która biblioteka okaże się lepsza, należy zapisać decyzję oraz powód w dokumencie README, aby nikt później nie dodał drugiej biblioteki tylko po to, by ją przetestować.
Zapisywanie środowiska przed porównaniem
Takie wyniki mają sens tylko w połączeniu z wersjami, które je wygenerowały. Środowiskiem referencyjnym było tutaj Node 24, TypeScript 7 i Next.js 16.3, uruchamiające małe aplikacje do obsługi faktur składające się z czterech tras. Zachowaj plik notes/lab.md w repozytorium i zacznij od zapisania tych trzech wersji:
node -v
pnpm exec tsc -v
pnpm exec next --version
Zapisz je na górze notatki. Jeśli główna wersja różni się od tej, którą zakłada przewodnik, zatrzymaj się i dopasuj je przed uruchomieniem czegokolwiek innego, ponieważ późniejsze polecenia mogą w inny sposób wprowadzić cię w błąd.
Następnie uruchom serwer rozwojowy i przejdź po trasach:
pnpm exec next dev
Odwiedź /, /invoices, /invoices/1, /settings, a potem ponownie /invoices, z włączoną opcją „Preserve log” w DevTools. Zapisz pole filtrów razem z adresem URL – to połączenie często stanowi niezbędne dowody później.
Następnie uruchom sprawdzacz typów i wydrukuj jego kod wyjścia:
pnpm exec tsc --noEmit --pretty false
echo $?
Kod wyjścia równy zero nie jest żadną funkcją, lecz jedynie uprawnieniem do przejścia do sprawdzeń w czasie wykonywania programu. Po tym uruchom polecenia z powyższej sekcji ćwiczeń na własnym komputerze, zamiast polegać na tych wynikach; sprzęt, presja pamięci oraz to, co robi przeglądarka, mogą bardziej wpłynąć na zużycie pamięci, czas trwania sprawdzania typów i czasy pobierania danych niż mała wersja frameworka.
Pomaga również zachowanie jednowierszowej notatki typu „naprawa nieudana” w formie „spróbowałem X, nadal miałem problem Y”. Taka notatka sprawia, że plik staje się wiernym zapisem ćwiczenia, a nie jedynie broszurą, i jest najbardziej przydatna, jeśli ma trafić do kolegi, który kontynuuje badania.
Błędy, których warto unikać
Cztery błędy wynikające z tego rodzaju porównań występują często:
- Uruchamianie obu narzędzi migracyjnych na tym samym bazie danych w celu porównania ich, co skutkuje dwiema historiami migracji oraz jedną tabelą o dwóch nazwach. Jedynym skutecznym sposobem przywrócenia jest odzyskanie danych z kopii zapasowej.
- Importowanie wygenerowanych enum Prisma do pliku, który następnie jest przetwarzany za pomocą funkcji usuwania typów w Node, co z powodów wymienionych wyżej nie działa. Lepiej skompilować ten pakiet.
- Ocenianie bibliotek na podstawie liczby pobrań, co nie ma żadnego wpływu na rodzaj połączenia między tabelami.
Lista kontrolna przed dodaniem ORM
- Jeden ORM na jedną bazę danych.
- Zapisanie zapytania SQL użytego do kluczowego połączenia przynajmniej raz.
- Zapisanie wartości
typeofkolumn finansowych przynajmniej raz, a także po każdej aktualizacji sterownika. - Kompilacja wygenerowanego pliku zawierającego enum, a nigdy jego uruchamianie poprzez bezpośrednie usuwanie typów.
- W dokumencie README podanie nazwy wybranej biblioteki oraz powodu jej wyboru.
Zakończenie
Dwie tabele nie stanowią gotowego schematu produkcyjnego, a to ćwiczenie nie obejmowało testowania tysięcy operacji połączeń ani wdrażania na środowisko edge runtime. Pokazuje natomiast, że decydujące różnice są konkretne i można je sprawdzić w ciągu popołudnia: forma wyniku połączenia, czytelność zapisanego kodu SQL, koszt kroku generowania oraz to, czy narzędzie zwraca liczby czy łańcuchy tekstowe. Wybierz jedną bibliotekę na każdą bazę danych, zapisz powód i sprawdzanie typeof total niech stanie się rutyną po każdej aktualizacji narzędzia. Pozwalanie dwóm narzędziom migracyjnym na zarządzanie jedną bazą danych kończy się koniecznością jej przywrócenia, więc trzymaj to doświadczenie z dala od wszelkich systemów obsługujących rzeczywiste pieniądze.
Literatura pokrewna
- Prisma dla rozwojowców Spring Boot: Mapowanie nawyków JPA do Node.js — Przewodnik dla programistów Java i JPA przechodzących na Node.js: jak modele, relacje, migracje i typy w Prismie odpowiadają znanych koncepcjom oraz co pozostaje do zrobienia.
- Migracja z Prismy na Drizzle: Sześciomiesięczna refleksja — Programista dzieli się rzeczywistymi wynikami testów i kompromisami związanymi ze zmianą stosu PostgreSQL TypeScript z Prismy na ORM Drizzle.