Prisma 7, czyli warstwa danych po przebudowie
Prisma przez lata była najwygodniejszą warstwą dostępu do bazy w ekosystemie Node, i miała jedną cechę, która budziła opór: pod spodem działał silnik napisany w Ruście, dostarczany jako plik binarny osobny dla każdej platformy.
Wersja siódma to zmieniła. Silnik zniknął, a jego zadania przejął kod w TypeScripcie z fragmentem skompilowanym do formatu uruchamianego w przeglądarce i na serwerze. Zmiana weszła jako domyślna pod koniec 2025 roku i pociągnęła za sobą kilka rzeczy, które trzeba zrobić przy aktualizacji.
Korzyści są wymierne. Producent podaje około dziewięćdziesiąt procent mniejszy rozmiar paczki, zapytania szybsze nawet trzykrotnie i wyraźnie szybsze sprawdzanie typów. Znika też problem, który przy wdrożeniach bezserwerowych i w środowiskach brzegowych bywał blokujący: konieczność dostarczenia właściwego pliku binarnego dla każdej platformy docelowej.
Co się zmieniło przy siódemce
Cztery zmiany, z których każda wymaga działania przy aktualizacji, więc warto poznać je przed rozpoczęciem.
Pierwsza to obowiązkowe adaptery sterowników. Klient nie łączy się już z bazą sam, tylko przez sterownik podany jawnie, więc każdy projekt potrzebuje odpowiedniego pakietu dla swojej bazy. To dodatkowa linia w kodzie i zarazem zysk, bo pulą połączeń zarządza teraz sterownik, który znasz.
Druga to miejsce generowanego kodu. Nie powstaje już w katalogu zależności, tylko w ścieżce, którą podajesz jawnie, a importy prowadzą do niej zamiast do nazwy pakietu. To oznacza zmianę importu w każdym pliku sięgającym po klienta i jest najbardziej pracochłonną częścią migracji.
Trzecia to plik konfiguracyjny. Adres bazy przenosi się ze schematu do osobnego pliku konfiguracyjnego, a generator zmienia nazwę. Pominięcie tego kroku daje błąd, którego treść nie wskazuje wprost przyczyny.
Czwarta to usunięcie warstwy pośredniczącej, która pozwalała opakowywać zapytania. Zastępują ją rozszerzenia klienta, dostępne już wcześniej, więc kod korzystający z tamtego mechanizmu wymaga przepisania.
Schemat i migracje
Sercem projektu pozostaje plik opisujący model danych, i to się nie zmieniło.
model Uzytkownik {
id String @id @default(cuid())
email String @unique
nazwa String?
zamowienia Zamowienie[]
utworzony DateTime @default(now())
}
model Zamowienie {
id String @id @default(cuid())
wartosc Decimal @db.Decimal(10, 2)
status Status @default(NOWE)
uzytkownik Uzytkownik @relation(fields: [uzytkownikId], references: [id])
uzytkownikId String
@@index([uzytkownikId, status])
}
enum Status {
NOWE
WYSLANE
ANULOWANE
}Z tego opisu powstają dwie rzeczy: migracja zmieniająca strukturę bazy i typy TypeScript dla całego kodu. Ta druga jest powodem, dla którego ludzie tu przychodzą, bo literówka w nazwie pola staje się błędem kompilacji zamiast pustej wartości odkrytej przez użytkownika.
npx prisma migrate dev --name dodaj-status-zamowieniaMigracje są zapisywane jako pliki z zapytaniami i trafiają do repozytorium, więc przechodzą przegląd i wykonują się na produkcji w kontrolowany sposób. To jest właściwy sposób pracy, a polecenie wypychające schemat bez migracji nadaje się wyłącznie do prototypu, bo nie zostawia śladu i potrafi usunąć kolumnę bez pytania.
Indeksy warto planować razem ze schematem, a nie po pierwszym zgłoszeniu o wolnej stronie. Deklaracja indeksu na parze kolumn używanych razem w zapytaniu to jedna linia w pliku schematu, a różnica przy milionie wierszy sięga sekund i widać ją w każdym zapytaniu filtrującym po tych polach.
Zapytania i relacje
Interfejs zapytań jest tu mocną stroną, bo czyta się go bez znajomości składni bazy.
const zamowienia = await prisma.zamowienie.findMany({
where: {
status: 'NOWE',
wartosc: { gte: 100 },
uzytkownik: { email: { endsWith: '@firma.pl' } }
},
include: { uzytkownik: { select: { nazwa: true, email: true } } },
orderBy: { utworzony: 'desc' },
take: 20
})Zagnieżdżony warunek na powiązanym modelu przekłada się na złączenie, a wybór pól ogranicza pobierane kolumny. Typ wyniku uwzględnia dokładnie to, co wybrałeś, więc sięgnięcie po pole spoza listy nie skompiluje się.
Najczęstszy problem wydajnościowy w tej warstwie nazywa się zapytaniem w pętli. Powstaje, gdy pobierasz listę, a potem dla każdego elementu osobno dociągasz powiązanie. Lista dwudziestu zamówień generuje wtedy dwadzieścia jeden zapytań zamiast jednego, a przy setce różnica staje się widoczna gołym okiem.
Lekarstwo mieści się w jednym słowie: dołączanie powiązań w tym samym zapytaniu, jak w przykładzie powyżej. Warto włączyć zapisywanie zapytań w środowisku deweloperskim, bo lista dwudziestu identycznych zapytań pod rząd rozpoznaje ten problem szybciej niż jakiekolwiek narzędzie.
Transakcje obsługują oba potrzebne warianty: prosty, gdzie kilka operacji wykonuje się razem albo wcale, oraz interaktywny, gdzie w środku transakcji podejmujesz decyzje na podstawie odczytanych danych. Ten drugi trzyma połączenie otwarte, więc długie operacje w środku transakcji są tu antywzorcem.
Migracje na produkcji
Ta część rozstrzyga o tym, czy warstwa dostępu jest wygodą, czy źródłem nocnych telefonów, a w dokumentacjach zajmuje zwykle mniej miejsca, niż powinna.
Zasada jest jedna i wynika z kolejności wdrożeń. Migracja schematu idzie przed wdrożeniem kodu, więc nowa kolumna musi istnieć, zanim aplikacja zacznie ją zapisywać. Zmiana usuwająca kolumnę idzie odwrotnie: najpierw kod przestaje jej używać, potem kolumna znika.
Z tego wynika sposób prowadzenia zmian nieodwracalnych. Zmiana nazwy kolumny wykonana wprost oznacza moment, w którym stary kod pisze do nieistniejącego pola. Bezpieczna droga to trzy wdrożenia: dodanie nowej kolumny i pisanie do obu, przeniesienie danych i przełączenie odczytu, usunięcie starej kolumny. Wolniej, za to bez przerwy w działaniu.
Warto też rozdzielić migracje zmieniające strukturę od tych przenoszących dane. Pierwsze są szybkie i przewidywalne, drugie potrafią trwać godzinami na dużej tabeli i blokować zapisy.
npx prisma migrate diff \
--from-config-datasource \
--to-schema=prisma/schema.prisma \
--script > migracja.sql
npx prisma migrate deployPierwsze polecenie pokazuje SQL, który powstanie, zanim cokolwiek się wykona. To jest ten moment, w którym widać instrukcję usuwającą kolumnę albo dodającą warunek niepustości do tabeli pełnej danych. Przeniesienie danych lepiej wykonać osobnym skryptem, partiami, poza wdrożeniem.
Ostatnia rzecz to sprawdzenie migracji na kopii danych produkcyjnych, a nie na pustej bazie deweloperskiej. Zapytanie dodające indeks wykonuje się natychmiast na tysiącu wierszy i kilkanaście minut na dziesięciu milionach, a tę różnicę lepiej poznać dzień wcześniej.
Wydajność w praktyce
Kilka rzeczy, które przy tej warstwie dają największą różnicę, a nie wymagają przebudowy niczego.
Pierwsza to wybieranie kolumn. Domyślnie pobierane są wszystkie, a przy szerokiej tabeli z polami tekstowymi to bywa różnica rzędu wielkości w ilości przesyłanych danych. Jawna lista pól to jedna linia.
Druga to stronicowanie po kursorze zamiast po przesunięciu. Pominięcie stu tysięcy wierszy wymaga od bazy ich przejrzenia, więc dziesiąta strona listy działa szybko, a tysięczna nie działa wcale.
const strona = await prisma.zamowienie.findMany({
take: 20,
skip: kursor ? 1 : 0,
cursor: kursor ? { id: kursor } : undefined,
orderBy: { id: 'desc' },
select: { id: true, numer: true, kwota: true, utworzono: true }
})Kursor oparty na indeksowanej kolumnie utrzymuje stały czas niezależnie od głębokości. Jawna lista pól w ostatniej linii załatwia przy okazji pierwszą z wymienionych rzeczy, bo bez niej pobierane są wszystkie kolumny, łącznie z polami tekstowymi, których lista nie pokazuje.
Trzecia to zapisy grupowe. Wstawienie tysiąca wierszy pojedynczo to tysiąc podróży do bazy, a jedno wywołanie wsadowe to jedna. Różnica bywa stukrotna i widać ją przy każdym imporcie danych.
Czwarta to liczenie. Policzenie wszystkich pasujących wierszy przy dużej tabeli bywa droższe niż pobranie samych danych, więc przy nieskończonym przewijaniu lepiej sprawdzać, czy istnieje kolejna strona, niż podawać dokładną liczbę wyników, której i tak nikt nie czyta.
Piąta to zapisywanie zapytań w środowisku deweloperskim. Włączenie go kosztuje jedną opcję przy tworzeniu klienta, a pokazuje wprost, ile zapytań generuje jedno wejście na stronę.
import { PrismaClient } from '../generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
export const prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development'
? [{ emit: 'event', level: 'query' }]
: ['warn', 'error']
})
let licznik = 0
prisma.$on('query', (e) => {
licznik += 1
if (e.duration > 100) console.warn(licznik, e.duration + 'ms', e.query)
})To najprostsze narzędzie diagnostyczne w całej tej warstwie i zarazem najrzadziej używane, bo działa dopiero wtedy, gdy ktoś na nie spojrzy. Licznik z progiem czasowym zmienia to: zamiast czytać setki linii, widzisz wyłącznie zapytania, które faktycznie kosztują.
Prisma kontra alternatywy
| Rozwiązanie | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| Prisma | Czytelny schemat, migracje, najlepsze typy | Osobny język schematu, cięższy niż konkurencja | Zespół chcący jednego źródła prawdy o modelu |
| Drizzle | Bliżej zapytań, lekki, schemat w TypeScripcie | Mniej gotowych ułatwień | Kontrola nad wygenerowanym zapytaniem |
| Konstruktory zapytań | Pełna kontrola, minimum abstrakcji | Typy i migracje po Twojej stronie | Złożone zapytania analityczne |
| Zapytania wprost | Zero warstw, pełna moc bazy | Brak typów, ręczne mapowanie | Nietypowe zapytania i optymalizacja |
Wybór między dwoma pierwszymi wierszami sprowadza się do tego, czy chcesz opisywać model w osobnym języku, czy w TypeScripcie. Pierwsze podejście daje jeden plik czytelny również dla osób nietechnicznych i lepsze narzędzia wokół migracji. Drugie daje mniejszą warstwę abstrakcji i zapytania bliższe temu, co faktycznie trafia do bazy.
Warto pamiętać, że to nie jest wybór wykluczający. Zdecydowana większość zapytań mieści się w interfejsie warstwy dostępu, a te kilka trudnych analitycznych da się napisać wprost, zachowując typy dzięki mechanizmowi typowanych zapytań surowych.
Trzeci wiersz zasługuje na uwagę przy raportowaniu. Zapytania z funkcjami okna, agregacjami po kilku wymiarach i podzapytaniami korelowanymi wychodzą w interfejsie warstwy dostępu sztucznie albo wcale, a napisane wprost są krótsze i czytelniejsze. Trzymanie ich w osobnym katalogu, obok reszty kodu dostępu do danych, sprawdza się lepiej niż próba zmieszczenia wszystkiego w jednym stylu.
Czwarty wiersz to nie jest porażka projektu, tylko normalna część pracy z bazą. Warstwa dostępu ma pokryć dziewięćdziesiąt procent przypadków wygodnie, a nie sto procent za wszelką cenę.
Praca z bazą u dostawcy
Warstwa dostępu współpracuje ze wszystkimi popularnymi usługami bazodanowymi, przy czym dwie rzeczy warto sprawdzić przed wyborem.
Pierwsza to liczba jednoczesnych połączeń przy wdrożeniu bezserwerowym. Każda instancja funkcji otwiera własne, więc limit potrafi zostać osiągnięty przy nagłym ruchu. Rozwiązaniem jest pośrednik zarządzający pulą, oferowany przez większość dostawców, w tym opisanych w tekstach o Neonie i PlanetScale.
Druga to obsługa kluczy obcych, bo nie wszędzie działa tak samo. Przy bazach, które ich nie wymuszają, warstwa dostępu potrafi emulować to zachowanie, więc kod wygląda znajomo, natomiast baza nie zatrzyma zapisu naruszającego powiązanie. To rozróżnienie warto znać, zanim oprzesz na nim założenia o spójności danych.
Przy aplikacji na Next.js dochodzi jeszcze jedna rzecz: pojedyncza instancja klienta. Tworzenie nowego klienta przy każdym przeładowaniu w trybie deweloperskim wyczerpuje pulę połączeń w kilka minut, a rozwiązaniem jest trzymanie instancji w obiekcie globalnym poza produkcją.
import { PrismaClient } from '../generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
const globalny = globalThis as unknown as { prisma?: PrismaClient }
export const prisma =
globalny.prisma ??
new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) })
if (process.env.NODE_ENV !== 'production') {
globalny.prisma = prisma
}Ten fragment powtarza się w niemal każdym projekcie i warto go dodać od razu, a nie po pierwszym komunikacie o wyczerpanej puli połączeń. Zwróć uwagę na ścieżkę importu prowadzącą do wygenerowanego katalogu oraz na adapter przekazany do konstruktora, bo to właśnie te dwie zmiany odróżniają siódemkę od wcześniejszych wersji i najczęściej umykają przy aktualizacji. Wywołanie bez adaptera nie jest już obsługiwane, więc stary zapis kończy się błędem, a nie cichym powrotem do dawnego zachowania.
Typowe błędy
Pierwszy to zapytanie w pętli, czyli dociąganie powiązań osobno dla każdego elementu listy. Dołączenie ich w jednym zapytaniu usuwa problem.
Drugi to wypychanie schematu bez migracji poza prototypem. Ta droga nie zostawia śladu w repozytorium i potrafi usunąć kolumnę bez pytania.
Trzeci to nowa instancja klienta przy każdym żądaniu. Pula połączeń wyczerpuje się wtedy szybciej, niż podpowiada intuicja, zwłaszcza w trybie deweloperskim z przeładowywaniem modułów.
Czwarty to zakładanie, że baza wymusi spójność powiązań, gdy robi to warstwa dostępu. Zapis z pominięciem tej warstwy przejdzie bez sprzeciwu.
Piąty to długie operacje wewnątrz transakcji interaktywnej. Połączenie zostaje zajęte, a przy większym ruchu blokuje kolejne żądania.
Szósty to aktualizacja do siódemki bez przeczytania listy zmian. Obowiązkowe adaptery sterowników, nowa ścieżka importów i przeniesienie adresu bazy do pliku konfiguracyjnego wymagają działania, a komunikaty błędów nie zawsze wskazują wprost przyczynę.
FAQ
Co zmieniła Prisma 7?
Silnik napisany w Ruście został zastąpiony kodem w TypeScripcie, co dało wyraźnie mniejszą paczkę, szybsze zapytania i szybsze sprawdzanie typów. Zniknęła też konieczność dostarczania osobnego pliku binarnego dla każdej platformy, co ułatwia wdrożenia bezserwerowe i brzegowe.
Jak wygląda migracja z szóstki?
Cztery kroki: dodanie adaptera sterownika dla swojej bazy, ustawienie ścieżki generowanego klienta i podmiana importów w całym projekcie, przeniesienie adresu bazy do pliku konfiguracyjnego oraz zastąpienie dawnej warstwy pośredniczącej rozszerzeniami klienta. Najbardziej pracochłonna jest podmiana importów.
Prisma czy Drizzle?
Zależy od tego, czy chcesz opisywać model w osobnym pliku schematu, czy w TypeScripcie. Prisma daje lepsze narzędzia wokół migracji i czytelny schemat, Drizzle daje cieńszą warstwę i zapytania bliższe temu, co trafia do bazy.
Czy działa w środowisku brzegowym?
Po zmianie architektury tak, i to jest jedna z głównych korzyści siódemki. Wymagany jest sterownik komunikujący się przez protokół sieciowy zamiast klasycznego gniazda, bo w takim środowisku zwykłe połączenie z bazą nie jest dostępne.
Czy mogę pisać zapytania wprost?
Tak, i przy złożonych zapytaniach analitycznych to najlepsza droga. Mechanizm typowanych zapytań surowych pozwala zachować typy wyniku, więc nie tracisz głównej zalety tej warstwy tylko dlatego, że jedno zapytanie wymagało pełnej mocy bazy.
Dokumentacja stoi na stronie projektu, a przewodnik aktualizacji opisuje oficjalna instrukcja przejścia na wersję siódmą.