Strona główna / Artykuły / Prisma dla deweloperów Spring Boot: mapowanie nawyków JPA na Node.js

Prisma dla deweloperów Spring Boot: mapowanie nawyków JPA na Node.js

Przewodnik dla programistów Java i JPA przechodzących na Node.js: jak modele, relacje, migracje i typy Prisma odpowiadają znanych koncepcjom oraz co pozostaje do zrobienia.

1866 słów

Gdy backend w Node.js po raz pierwszy musi przechowywać dane w PostgreSQL, stajemy przed znanym dylematem: napisać surowy SQL, przyjąć tradycyjny ORM lub użyć narzędzia opartego na schemacie, takiego jak Prisma. Dla programistów przychodzących z środowiska Java, Spring Boot, JPA i Hibernate wybór dotyczy również przeniesienia już sprawdzonego modelu myślowego oraz czystej warstwy dostępu do danych, która chroni SQL przed obsługą żądań. Ten przewodnik wykorzystuje mały backend do nagrywania głosu jako przykład, aby pokazać, w jaki sposób koncepcje Prismy pokrywają się z tym, co znasz z JPA, gdzie się różnią oraz jakie umiejętności dotyczące bazy danych żaden ORM nie może zastąpić.

Miejsce Prismy w architekturze

Prisma to ORM oraz zestaw narzędzi do pracy z bazami danych dla Node.js i TypeScript. Znajduje się pomiędzy kodem aplikacji a bazą danych:

Node.js / TypeScript API
  ↓
Prisma
  ↓
PostgreSQL

PostgreSQL pozostaje główną bazą danych, odpowiedzialną za przechowywanie danych, ograniczenia, transakcje oraz wykonywanie zapytań. Prisma umożliwia aplikacji strukturyzowane i typizowane sposoby komunikacji z tą bazą.

Bez ORM wyszukiwanie użytkownika po adresie e-mail oznacza konieczność pisania kodu SQL bezpośrednio:

SELECT *
FROM users
WHERE email = 'user@example.com';

Z Prismą to samo wyszukiwanie wygląda jak zwykły kod TypeScript. Funkcja findUnique przyjmuje tylko te pola, które w schemacie są oznaczone jako unikalne lub jako klucz główny, dzięki czemu kompilator wie, że to zapytanie zwróci co najwyżej jedną wiersz.

const user = await prisma.user.findUnique({
  where: {
    email: "user@example.com"
  }
});

Jeśli korzystałeś z repositoriów Spring Data JPA, ten styl będzie ci znajomy: wywołujesz metodę w dostępniku specyficznym dla modelu zamiast ręcznie budować zapytanie.

Jak te koncepcje porównują się z Spring Data JPA

Dwa te ekosystemy nie odpowiadają sobie jeden do jednego, ale obowiązki, które przyjmują, są takie same. Kod specyficzny dla bazy danych nie powinien przenikać do każdej części aplikacji; należy go umieścić w dedykowanej warstwie dostępu do danych. Największa różnica strukturalna dotyczy miejsca, w którym definiowany jest model. JPA wywodzi mapowanie z klasyfikowanych klas Java, natomiast Prisma wykorzystuje oddzielny plik schematu jako jedyny źródło prawdy i na jego podstawie generuje klienta.

Definicja modelu

W Spring Boot entytą użytkownika jest klasa z adnotacjami:

@Entity
public class User {

    @Id
    private Long id;

    private String email;

    private String name;
}

W Prismie odpowiednik znajduje się w pliku schema.prisma. Atrybuty @id i @default(autoincrement()) pełnią rolę adnotacji @Id z JPA, ale z wartością generowaną automatycznie, natomiast @unique tworzy rzeczywisty ograniczenie unikalności w bazie danych.

model User {
id Int @id @default(autoincrement())
email String @unique
name String }

Należy pamiętać, że format schematu Prisma wymaga, aby każde pole znajdowało się na osobnej linii, a zamykająca nawiaska również na oddzielnej linii; w rzeczywistym pliku taka kompaktowa forma musi być zorganizowana w ten sposób. To właśnie ten schemat jest czytany zarówno przez narzędzia do migracji, jak i generowany klient, dlatego stanowi autorytatywny opis tego, w jaki sposób aplikacja postrzega bazę danych.

Typy generowane jako zabezpieczenie

Najbardziej zauważalną cechą jest ścisła integracja Prismy z TypeScriptem. Uruchomienie polecenia prisma generate tworzy klienta, którego metody i typy zwracanych wartości pochodzą od Twoich modeli. Prosta zapytanie wygląda w ten sposób:

const users = await prisma.user.findMany();

zwraca obiekty, o których TypeScript wie, że zawierają dokładnie te pola:

id
email
name

W praktyce oznacza to autodopasowywanie tekstu w edytorze, sprawdzanie typów we każdym polu, które odczytujesz lub filtrowasz, oraz błędy w czasie kompilacji, gdy kolumna jest przemieniana nazwą w schemacie, ale nie w kodzie. W większych systemach backendowych eliminuje to całą kategorię błędów wynikających z pomyłek pisarskich.

Tworzenie rekordów

Załóżmy, że aplikacja do nagrywania musi przechowywać nagrania dźwiękowe. Model z kluczem głównym typu UUID oraz datą utworzenia, którą uzupełnia baza danych, wygląda następująco:

model Recording {
  id        String   @id @default(uuid())
  title     String
  audioUrl  String
  createdAt DateTime @default(now())
}

Wstawienie wiersza polega wtedy na jednej wywołaniu. Przekazujesz tylko pola bez wartości domyślnych, a Prisma zwraca pełny utworzony rekord, w tym wygenerowane wartości id i createdAt:

const recording = await prisma.recording.create({
data: { title: "Project Meeting",
        audioUrl: "/audio/project-meeting.mp3"
} });

Robienie tego ręcznie oznaczałoby napisanie zapytania INSERT, powiązanie parametrów, odczytanie wygenerowanych wartości oraz przekształcenie wiersza w obiekt.

Modelowanie relacji

Rzeczywiste schematy rzadko składają się z izolowanych tabel. W tym przypadku jeden użytkownik posiada wiele nagrań. W Prisma związek jest deklarowany po obu stronach: pole typu lista w User, a w Recording skalarowy klucz obcy wraz z polem związkowym określającym, które kolumny łączą te dwa obiekty.

model User {
id String @id @default(uuid())
email String @unique
name String recordings
Recording[]
}
model Recording {
id String @id @default(uuid())
title String
audioUrl String
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id])
}

Podobnie jak w poprzednim modelu, powyższa struktura jest skompresowana. W funkcjonalnym schemacie pole typu lista jest zapisywane na jednej linii jako recordings Recording[], a każde inne pole również ma własną linię. Tylko userId staje się rzeczywistą kolumną; recordings i user to pola wirtualne, które istnieją w klientach do celów nawigacji.

Gdy związek jest już ustawiony, można utworzyć nagranie należące do konkretnego użytkownika, ustawiając bezpośrednio klucz obcy:

const recording = await prisma.recording.create({
data: {
   title: "Daily Standup",
   audioUrl: "/audio/standup.mp3",
   userId
      }
});

Dla deweloperów JPA odpowiada to @OneToMany po stronie użytkownika oraz @ManyToOne po stronie zapisów. Jeden praktyczny różnicę: w PostgreSQL Prisma nie dodaje automatycznie indeksu do kolumny klucza obcego, dlatego warto dodać @@index([userId]) do modelu Recording, jeśli często będziesz wyszukiwać zapisy według ich właściciela.

Ewolucja schematu za pomocą migracji

Schematy się zmieniają. Wyobraźmy sobie, że pierwsza wersja tabeli użytkowników zawiera tylko te kolumny:

id
email
name

a później musisz dodać pole z datą i godziną:

createdAt

Ręczna edycja bazy danych produkcyjnej to dokładnie to, czego należy unikać. Narzędzia do migracji Prismy porównują Twoją strukturę z historią migracji i generują pliki SQL dla każdej zmiany. W środowisku rozwojowym prisma migrate dev tworzy i aplikuje te pliki; w środowisku produkcyjnym prisma migrate deploy aplikuje te jeszcze niezrealizowane zmiany, bez tworzenia niczego nowego. Pliki SQL znajdują się obok kodu źródłowego, więc zmiany w bazie danych przechodzą przez proces przeglądu kodu i historię Git tak jak każda inna zmiana, podobnie jak to robią Flyway lub Liquibase w projekcie Spring.

Kiedy surowy SQL nadal jest lepszym narzędziem

Surowy SQL nie jest wrogiem, a zrozumienie języka SQL pozostaje niezbędne. Zapytania napisane ręcznie często są lepszym rozwiązaniem w przypadku:

  • złożonych zapytań analitycznych
  • operacji silnie optymalizowanych
  • zapytań do generowania raportów
  • cechy specyficzne dla PostgreSQL
  • Dla rutynowych operacji aplikacyjnych, takich jak te poniżej, ORM eliminuje dużo powtarzalnego kodu:

    Create user
    Get user
    Update recording
    Delete session
    List transcripts
    Find recording by ID
    

    Celem nie jest wyeliminowanie SQL z projektu. Chodzi o to, aby proste operacje CRUD pozostały proste, przy jednoczesnym zrozumieniu tego, co dzieje się na poziomie bazy danych. Prisma oferuje również $queryRaw w przypadkach, gdy potrzebna jest praca z SQL bez opuszczania klienta.

    Dlaczego Prisma zamiast innych opcji Node.js

    Ekosystem Node.js oferuje wiele bibliotek do pracy z bazami danych, z których każda ma swoje wady i zalety:

    Prisma
    Drizzle ORM
    TypeORM
    Sequelize
    Knex
    node-postgres
    

    Dla programisty przychodzącego z Spring Boot, atutem Prismy jest przede wszystkim dobra experiencia programisty oraz pierwszorzędne wsparcie dla TypeScript. Zachęca ona również do myślenia warstwowego, które odpowiada typowej aplikacji Spring:

    Model
      ↓
    Data Access
      ↓
    Service
      ↓
    API
    

    Zamiast rozproszać SQL w różnych obsługach API. Jeśli chcesz szerszego porównania opcji, w tym sytuacji, gdy budownik zapytań jest lepszym wyborem, zapoznaj się z instrukcjami dotyczącymi wyboru między surowym SQL, Prismą a Drizzle.

    Włączenie Prismy do szerszej architektury

    W aplikacji do nagrywania Prisma jest odpowiedzialna za dane relacyjne, takie jak:

    Users
    Recordings
    Sessions
    Transcripts
    Metadata
    Processing jobs
    

    W miarę rozwoju systemu architektura może ewoluować w kierunku czegoś takiego, z warstwą usługową pomiędzy API a kodem dostępu do danych:

    React / Next.js Frontend
            ↓
    Node.js / TypeScript API
            ↓
    Service Layer
            ↓
    Prisma
            ↓
    PostgreSQL
    

    Późniejsze etapy mogą wprowadzić zupełnie inne komponenty:

    Object Storage
    Redis
    Message Queues
    AI Transcription Services
    Background Workers
    

    Prisma nie zastępuje żadnego z tych elementów: audio powinno być przechowywane w systemie przechowywania obiektów, cache w Redis, a transkrypcja w procesach obsługiwanych przez kolejki. Prisma obejmuje wyłącznie warstwę relacyjną.

    Część przenoszalna: przepływ danych

    Nauka API Prismy to mniej ważna kwestia. Ważniejsze jest zrozumienie, jak dane przemieszczają się w tle od żądania do rekordu:

    HTTP Request
         ↓
    Controller / Route
         ↓
    Service
         ↓
    Repository / Prisma
         ↓
    PostgreSQL
    

    W praktyce tworzenie zapisu odbywa się według następującej ścieżki:

    POST /recordings
         ↓
    Recording Controller
         ↓
    Recording Service
         ↓
    Prisma
         ↓
    INSERT INTO recordings
    

    Przepływ ten pozostaje taki sam niezależnie od używanego ORM lub języka, dlatego też dane mogą być przenoszone.

    Czego ORM dla ciebie nie robi

    Prisma nie zastępuje PostgreSQL, dobrego projektu schematu ani znajomości SQL. Nadal potrzebujesz solidnej bazy wiedzy na temat:

    Indexes
    Constraints
    Primary keys
    Foreign keys
    Transactions
    Joins
    Normalization
    Query performance
    Locking
    Connection pooling
    

    ORM ułatwia dostęp do danych; nie sprawia, że słabo zindeksowana tabela będzie szybka ani że brak ograniczeń zapewni bezpieczeństwo. Poolowanie połączeń wymaga szczególnej uwagi w Node.js, ponieważ tworzenie wielu instancji klienta, na przykład przy każdym hot reloadzie lub wywołaniu bezserwerowym, może wyczerpać połączenia z PostgreSQL.

    Podsumowanie

    Dla backendu napisanego w TypeScript na bazie PostgreSQL Prisma zapewnia przydatną równowagę pomiędzy produktywnością a zrozumieniem mechanizmów, umożliwiając deweloperom Spring Boot ponowne wykorzystanie większości swoich instynktów architektonicznych. Ważniejszą umiejętnością jest nie zapamiętywanie konkretnych wywołań, takich jak to:

    prisma.user.findMany();
    

    Jest to zrozumienie tego, jak żądanie przemieszcza się od punktu końcowego API do bazy danych relacyjnej i z powrotem. Rozsądnym następnym krokiem jest stworzenie pierwszych prawdziwych modeli, połączenie ich z PostgreSQL, utworzenie początkowej migracji oraz udostępnienie danych poprzez REST API.

    • Traktuj schema.prisma jako jedyny źródło prawdy dotyczące modeli, relacji i ograniczeń.
    • Polegaj na generowanym kliencie dla bezpieczeństwa typów i regeneruj go za każdym razem, gdy zmienia się schemat.
    • Używaj migrate dev lokalnie oraz migrate deploy w środowisku produkcyjnym, aby każda zmiana schematu była wersjonowana.
  • Zachowaj surowy SQL do analiz, raportowania oraz funkcji specyficznych dla bazy danych.
  • Kontynuuj inwestycje w indeksy, ograniczenia, transakcje oraz wydajność zapytań; ORM nie zajmuje się tym za ciebie.