Головна / Статті / Prisma для розробників Spring Boot: відповідність звичок JPA та Node.js

Prisma для розробників Spring Boot: відповідність звичок JPA та Node.js

Посібник для розробників Java та JPA, які переходять на Node.js: як моделі Prisma, взаємозв’язки, міграції та типи відповідають знайомим концепціям, та що залишається для вас.

1866 слів

Коли сервер Node.js вперше потребує зберігання даних у PostgreSQL, виникає знайомий вибір: писати сирий SQL, використовувати традиційний ORM чи інструментарій, орієнтований на схему, як-от Prisma. Для розробників, які прийшли з Java, Spring Boot, JPA та Hibernate, вибір також стосується перенесення вже ефективної ментальної моделі та чистого шару доступу до даних, який утримує SQL поза обробниками запитів. У цьому посібнику як приклад використовується невеликий сервер для запису голосу, щоб показати, як концепції Prisma відповідають тому, що ви знаєте з JPA, де вони відрізняються, та які навички роботи з базами даних жоден ORM не може замінити.

Місце Prisma у стеку

Prisma — це ORM та інструментарій для роботи з базами даних для Node.js та TypeScript. Він знаходиться між кодом вашого додатку та базою даних:

Node.js / TypeScript API
  ↓
Prisma
  ↓
PostgreSQL

PostgreSQL залишається основною базою даних, відповідальною за зберігання даних, обмеження, транзакції та виконання запитів. Prisma надає додатку типований, структурований спосіб взаємодії з нею.

Без ORM для пошуку користувача за електронною поштою доводиться писати SQL безпосередньо:

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

З Prisma такий пошук виглядає як звичайний TypeScript. Функція findUnique приймає лише ті поля, які схема позначила як унікальні або як первинний ключ, тож компілятор знає, що цей запит поверне не більше одного рядка.

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

Якщо ви використовували репозиторії Spring Data JPA, цей стиль здасться вам знайомим: ви викликаєте метод у спеціалізованому доступнику моделі замість того, щоб вручну складати запит.

Як ці концепції порівнюються з Spring Data JPA

Ці два екосистеми не відповідають один-одному за принципом взаємного зіставлення, але обов’язки, які вони беруть на себе, є однаковими. Код, специфічний для бази даних, не повинен потрапляти до кожної частини додатку; він має знаходитися у спеціалізованому шарі доступу до даних. Найбільша структурна відмінність полягає у місці визначення моделі. JPA отримує інформацію про зіставлення з анотованих класів Java, тоді як Prisma використовує окремий файл схеми як єдине джерело істини та генерує з нього клієнтський код.

Визначення моделі

У Spring Boot ентитет користувача — це анотований клас:

@Entity
public class User {

    @Id
    private Long id;

    private String email;

    private String name;
}

У Prisma еквівалент цьому знаходиться у файлі schema.prisma. Атрибути @id та @default(autoincrement()) виконують функцію атрибута @Id у JPA з генерованою значенням, а @unique стає справжнім обмеженням унікальності в базі даних.

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

Зверніть увагу, що формат схеми Prisma вимагає, щоб кожне поле знаходилося на окремій лінії, а закриваюча дужка — на окремій лінії; компактне представлення, подібне до наведеного вище, у справжньому файлі також має бути організоване в цьому форматі. Саме ця схема використовується як інструментами міграцій, так і генерованим клієнтом, тому вона є офіційним описом того, як додаток бачить базу даних.

Генеровані типи як запобіжний захід

Першою рисою, яку більшість людей помічає, є тісна інтеграція Prisma з TypeScript. Виконання команди prisma generate створює клієнт, чиї методи та типи значень походять від ваших моделей. Простий запит, подібний до цього:

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

повертає об’єкти, про які TypeScript знає, що вони містять саме ці поля:

id
email
name

На практиці це означає автодоповнення в редакторі, перевірку типів для кожного поля, яке ви читаєте або фільтруєте, та помилки на етапі компіляції, коли стовпець у схемі перейменовується, але не в коді. У більшому бекенді це усуває цілу категорію помилок рівня опечаток.

Створення записів

Припустимо, що додаток для запису потребує зберігання аудіозаписів. Модель із первинним ключем у форматі UUID та часом створення, який заповнює база даних, виглядає так:

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

Вставка рядка тоді — це один виклик. Ви передаєте лише поля без значень за замовчуванням, а Prisma повертає повний створений запис, включаючи згенеровані значення id та createdAt:

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

Якби це робити вручну, довелося б написати команду INSERT, прив’язати параметри, зчитати згенеровані значення та перетворити рядок у об’єкт.

Моделювання взаємозв’язків

Реальні схеми рідко складаються з ізольованих таблиць. Тут один користувач володіє багатьма записами. У Prisma зв’язок оголошується з обох сторін: поле списку у User та у Recording — скалярний іноземний ключ разом із полем зв’язку, яке вказує, які стовпці поєднують їх.

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])
}

Як і у попередній моделі, наведена вище структура є скороченою. У функціональній схемі поле списку записується в один рядок у вигляді recordings Recording[], а кожне інше поле також має власний рядок. Лише userId стає справжнім стовпцем; recordings та user — це віртуальні поля, які існують у клієнті для навігації.

З наявним зв’язком можна створити запис, який належить певному користувачеві, безпосередньо встановивши іноземний ключ:

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

Для розробників JPA це відповідає @OneToMany з боку користувача та @ManyToOne з боку запису. Одна практична відмінність: у PostgreSQL Prisma не додає автоматично індекс для стовпця іноземного ключа, тому варто додати @@index([userId]) до моделі Recording, якщо ви часто будете шукати записи за їхнім власником.

Еволюція схеми за допомогою міграцій

Схеми змінюються. Уявіть, що перша версія таблиці користувачів містить лише ці стовпці:

id
email
name

а пізніше вам потрібно додати часовий маркер:

createdAt

Редагування бази даних у продакшені вручну — саме цього слід уникати. Інструменти міграцій Prisma порівнюють вашу схему з історією міграцій та генерують SQL-файли для кожної зміни. Під час розробки команда prisma migrate dev створює та застосовує ці файли; у продакшені команда prisma migrate deploy застосовує незавершені міграції, не створюючи нічого нового. SQL-файли знаходяться поруч із вашим вихідним кодом, тож зміни до бази даних проходять перевірку коду та фіксуються в історії Git, як і будь-які інші зміни, подібно до того, як це роблять Flyway чи Liquibase у проектах на Spring.

Коли чистий SQL все ще є кращим інструментом

Чистий SQL — це не ворог, і розуміння SQL залишається необхідним. Ручно написані запити часто є кращим варіантом для:

  • складних аналітичних запитів
  • сильно оптимізованих операцій
  • запитів для створення звітів
  • функції, специфічні для PostgreSQL
  • Для рутинних операцій з додатком, подібних до наведених нижче, ORM допомагає скоротити кількість повторюваного коду:

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

    Мета не в тому, щоб повністю позбутися SQL у проекті. Йдеться про те, щоб зробити звичайні операції CRUD простими, водночас залишаючись у курсі того, що відбувається на рівні бази даних. Prisma також пропонує $queryRaw у випадках, коли потрібно скористатися SQL без виходу з клієнта.

    Чому Prisma замість інших варіантів Node.js

    Екосистема Node.js пропонує багато бібліотек для роботи з базами даних, кожна з яких має свої переваги та недоліки:

    Prisma
    Drizzle ORM
    TypeORM
    Sequelize
    Knex
    node-postgres
    

    Для розробника, який працював з Spring Boot, привабливістю Prisma є насамперед його інтерфейс для розробників та підтримка TypeScript першого класу. Він також спонукає до ланцюгового підходу, який відповідає типовому додатку Spring:

    Model
      ↓
    Data Access
      ↓
    Service
      ↓
    API
    

    замість розкидання SQL-запитів між обробниками API. Якщо ви хочете більш повне порівняння варіантів, включаючи ситуації, коли кращим вибором є конструктор запитів, дивіться як вибрати між сирим SQL, Prisma та Drizzle.

    Інтеграція Prisma у загальну архітектуру

    У додатку для запису Prisma відповідає за реляційні дані, такі як:

    Users
    Recordings
    Sessions
    Transcripts
    Metadata
    Processing jobs
    

    У міру зростання системи архітектура може еволюціонувати до щось на кшталт цього, зі шаром сервісів між API та кодом для доступу до даних:

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

    На пізніших етапах можуть з’явитися інші компоненти:

    Object Storage
    Redis
    Message Queues
    AI Transcription Services
    Background Workers
    

    Prisma не замінює жодного з цих елементів: аудіо зберігається у об’єктному сховищі, кеш — у Redis, а транскрипція обробляється за допомогою роботів з чергою. Prisma контролює лише реляційний шар.

    Та частина, яку можна перенести: потік даних

    Вивчення API Prisma — це менш важливий етап. Набагато важливіше зрозуміти, як дані переміщуються через бекенд від запиту до запису:

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

    Конкретно, створення запису відбувається за таким маршрутом:

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

    Цей потік залишається незмінним незалежно від того, який ORM чи мова ви використовуєте, саме тому його можна перенести.

    Що ORM не робить за вас

    Prisma — це не заміна PostgreSQL, якісному проектуванню схеми чи знанням SQL. Вам все одно потрібне міцне розуміння:

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

    ORM полегшує доступ до даних; він не робить погано індексовану таблицю швидкою та не робить відсутні обмеження безпечними. Пулінг з’єднань потребує особливої уваги в Node.js, оскільки створення багатьох екземплярів клієнта, наприклад під час кожної швидкої перезавантаження чи виклику без сервера, може вичерпати з’єднання PostgreSQL.

    Підсумок

    Для TypeScript-бекенду на PostgreSQL Prisma забезпечує корисний баланс між продуктивністю та зрозумілістю, дозволяючи розробникам Spring Boot повторно використовувати більшість своїх архітектурних навичок. Справжньою навичкою є не запам’ятовування таких викликів:

    prisma.user.findMany();
    

    Це розуміння того, як запит проходить від кінцевої точки API до реляційної бази даних та назад. Логічним наступним кроком є створення перших справжніх моделей, підключення їх до PostgreSQL, генерація початкової міграції та доступ до даних через REST API.

    • Вважайте schema.prisma єдиним джерелом істини для моделей, взаємозв’язків та обмежень.
    • Покладайтеся на генерований клієнт для забезпечення типобезпеки та генеруйте його знову щоразу, коли змінюється схема.
    • Використовуйте migrate dev локально та migrate deploy у продакшені, щоб кожна зміна схеми мала версіювання.
  • Зберігайте необроблений SQL для аналітики, звітності та функцій, специфічних для баз даних.
  • Продовжуйте інвестувати у індекси, обмеження, транзакції та продуктивність запитів; ORM не буде робити це замість вас.