Главная / Статьи / 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 — это не враг, и понимание его синтаксиса по-прежнему крайне важно. Ручно написанные запросы часто более подходят для:

  • сложных аналитических запросов
  • тщательно оптимизированных операций
  • запросов для генерации отчётов
  • функции, специфичные для 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, поскольку создание множества экземпляров клиента, например при каждой перезагрузке или вызове серверless-функции, может исчерпать подключения к PostgreSQL.

    Заключение

    Для типскриптового бэкенда на PostgreSQL Prisma обеспечивает полезный баланс между производительностью и пониманием сути, позволяя разработчикам Spring Boot использовать большинство своих архитектурных навыков. Настоящей компетенцией является не запоминание подобных вызовов:

    prisma.user.findMany();
    

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

    • Рассматривайте schema.prisma как единственный источник правды для моделей, связей и ограничений.
    • Опирайтесь на генерированный клиент для обеспечения типобезопасности и генерируйте его заново при любых изменениях схемы.
    • Используйте migrate dev в локальной среде и migrate deploy в производственной среде, чтобы каждое изменение схемы имело версию.
  • Сохраняйте необработанный SQL для аналитики, отчётности и функций, специфичных для базы данных.
  • Продолжайте инвестировать в индексы, ограничения, транзакции и производительность запросов; ORM не будет делать это за вас.