Обзор Prisma ORM

vxk59qa3zzqpy_maac9wkyp_yc8.png

Это статья-обзор о Prisma ORM.

ORM (англ. Object-Relational Mapping — «объектно-реляционное отображение или преобразование») — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».

Работа с базами данных (моделирование данных, изменение схем, формирование запросов и т.п.) — одна из наиболее сложных задач, возникающих при разработке приложений. Prisma предлагает решение, позволяющее сосредоточиться на данных вместо SQL.

Что такое Prisma?


Как утверждают разработчики, Prisma представляет собой «открытую ORM нового поколения для Node.js и TypeScript», реализующую «новую парадигму объектно-реляционного отображения».

Поддерживаемые языки программирования:

  • JavaScript
  • TypeScript
  • Go (в разработке)

Поддерживаемые базы данных:
  • MySQL
  • PostgreSQL
  • SQLite
  • MSSQL (в разработке)
  • MongoDB Connector (в разработке)

Предоставляемые инструменты:
  • Prisma Client: автоматически генерируемый и типобезопасный клиент для БД
  • Prisma Migrate: декларативное моделирование данных и настраиваемые миграции
  • Prisma Studio: современный пользовательский интерфейс для просмотра и редактирования данных
  • Prisma VSCode Extension: расширение для VSCode, обеспечивающее подсветку синтаксиса, автозавершение, быстрые исправления и др.

Prisma Client может быть использован в любом Node.js или TypeScript серверном приложении. Это может быть REST API, GraphQL API, gRPC API и т.д.

Как Prisma работает?


Все начинается с определения схемы. Схема позволяет разработчикам определять модели с помощью интуитивно понятного языка. Она также содержит соединение с БД и определяет генератор:

datasource db {

provider = «postgresql»

url = env («DATABASE_URL»)

}

generator client {

provider = «prisma-client-js»

}

model Post {

id Int @id @default (autoincrement ())

title String

content String?

published Boolean @default (false)

author User? @relation (fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default (autoincrement ())

email String @unique

name String?

posts Post[]

}

Каждая модель привязана к таблице в БД и является основой для генерируемого Prisma Client интерфейса доступа к данным.

В приведенной схеме мы настраиваем следующее:

  • Источник данных (data source): определяет соединение с БД (через переменную среды окружения)
  • Генератор (generator): сообщает, что мы хотим сгенерировать Prisma Client
  • Модель данных (data model): определяет модели приложения

Модель данных — это коллекция моделей. Главными задачами моделей является следующее:
  • Представление таблицы в БД
  • Предоставление основы для запросов Prisma Client

Для использования Prisma Client прежде всего необходимо установить соответствующий пакет из npm:

yarn add @prisma/client

# или

npm i @prisma/client

Установка данного пакета вызывает команду prisma generate, которая читает схему и генерирует код клиента. После генерации клиента, мы можем импортировать его в наш код и использовать для отправки запросов:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient ()

Миграции


Prisma Migrate преобразует схему в SQL для создания/изменения/удаления таблиц в БД. Миграция выполняется с помощью команды prisma migrate, предоставляемой Prisma CLI. Вот как выглядит SQL для приведенных выше моделей (SQLite):

CREATE TABLE «Post» (

«id» INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

«title» TEXT NOT NULL,

«content» TEXT,

«published» BOOLEAN NOT NULL DEFAULT false,

«authorId» INTEGER,

FOREIGN KEY («authorId») REFERENCES «User» («id») ON DELETE SET NULL ON UPDATE CASCADE

);

CREATE TABLE «User» (

«id» INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

«email» TEXT NOT NULL,

«name» TEXT

);

CREATE UNIQUE INDEX «User.email_unique» ON «User»(«email»);

Доступ к данным


Prisma Client позволяет разработчикам мыслить в категориях объектов. Другими словами, вместо концепции экземпляров модели, в ответ на запрос к БД возвращаются обычные JavaScript-объекты. Кроме того, запросы являются полностью типизированными. Рассмотрим несколько примеров:

// Получение всех постов

const posts = await prisma.post.findMany ()

// Получение всех постов и их авторов

const postsWithAuthors = await prisma.post.findMany ({

include: { author: true }

})

// Создание нового пользователя с новым постом

const userWithPosts = await prisma.user.create ({

data: {

email: 'john@mail.com',

name: 'John Smith',

posts: {

create: [{ title: 'Hello World' }]

}

}

})

// Получение всех пользователей с именем John

const users = await prisma.user.findMany ({

where: {

name: { contains: 'John' }

}

})

// Получение всех постов определенного пользователя

const postsByUser = await prisma.user.findUnique ({

where: { email: 'john@mail.com' }

}).posts ()

// Пагинация

const posts = await prisma.post.findMany ({

take: 5,

cursor: { id: '3' }

})

С полным описанием API можно ознакомиться здесь.

Быстрый старт


В данном разделе мы научимся отправлять запросы к базе данных SQLite на TypeScript с помощью Prisma Client.

Загрузка начального проекта и установка зависимостей


Копируем репозиторий с начальным проектом (на самом деле в данном репозитории находится 2 проекта, один на JavaScript, другой на TypeScript; разница между ними невелика):

git clone https://github.com/prisma/quickstart.git

Переходим в директорию, с которой мы будем работать, устанавливаем зависимости и открываем директорию в редакторе кода:

# Переключаем рабочую директорию

cd quickstart/typescript/starter

# Устанавливаем зависимости

yarn

# или

npm i

# Открываем директорию в редакторе кода

code .

Проект состоит из 6 файлов:

  • package.json: определяет настройки проекта (название, описание, зависимости, команды и т.д.)
  • prisma/schema.prisma: схема, в которой, в том числе, определяются наши модели
  • prisma/.env: определяет URL для соединения с базой данных в качестве переменной среды окружения
  • prisma/dev.db: файл БД SQLite
  • script.ts: исполняемый скрипт TypeScript
  • tsconfig.json: настройки компилятора TypeScript

Зависимости проекта:
  • prisma: Prisma CLI, который можно вызывать с помощью npx prisma
  • @prisma/client: Prisma Client для доступа к БД
  • typescript: набор инструментов TypeScript
  • ts-node: используется для запуска скрипта TypeScript

Файл prisma/dev.db содержит две таблицы с фиктивными данными:

Обратите внимание: колонка authorId содержит ссылку на таблицу User, т.е. 2 в колонке authorId таблицы Post — это ссылка на 2 в колонке id таблицы User.

Формирование запроса


Перед тем, как писать запрос к БД с помощью Prisma Client, взглянем на нашу схему:

datasource db {

provider = «sqlite»

url = env («DATABASE_URL»)

}

generator client {

provider = «prisma-client-js»

}

model Post {

id Int @id @default (autoincrement ())

title String

content String?

published Boolean @default (false)

author User? @relation (fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default (autoincrement ())

email String @unique

name String?

posts Post[]

}

Файл script.ts на данном этапе выглядит следующим образом:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient ()

async function main () {

// здесь будут находиться наши запросы

}

main ()

.catch (e => {

throw e

})

.finally (async () => {

await prisma.$disconnect ()

})

Начнем с запроса на получение всех пользователей:
async function main() {

const allUsers = await prisma.user.findMany ()

console.log (allUsers)

}

Выполним этот код с помощью следующей команды:

yarn dev

# или

npm run dev

Вот что должно появиться в терминале:

[

{ id: 1, email: 'sarah@prisma.io', name: 'Sarah' },

{ id: 2, email: 'maria@prisma.io', name: 'Maria' },

]

Одной из наиболее важных возможностей, предоставляемых Prisma Client, является обеспечение легкой работы с отношениями между данными. Для получения постов определенного пользователя достаточно применить настройку include. Изменим код функции main():

async function main() {

const allUsers = await prisma.user.findMany ({

include: { posts: true }

})

// Используем `console.dir` для правильного отображения вложенных объектов

console.dir (allUsers, { depth: null })

}

Выполняем код:

yarn dev

# или

npm run dev

Каждый объект теперь содержит свойство posts, представляющее данные, полученные благодаря наличию связи между таблицами User и Post:

[

{ id: 1, email: 'sarah@prisma.io', name: 'Sarah', posts: [] },

{

id: 2,

email: 'maria@prisma.io',

name: 'Maria',

posts: [

{

id: 1,

title: 'Hello World',

content: null,

published: false,

authorId: 2,

}

]

}

]

Обратите внимание, что переменная allUsers является строго типизированной благодаря типам, автоматически генерируемым Prisma Client. В этом можно убедиться, если навести курсор на allUsers в редакторе кода (VSCode или любом другом со встроенной поддержкой TypeScript):

const allUsers: (User & {

posts: Post[]

})[]

export type Post = {

id: number

title: string

content: string | null

published: boolean

authorId: number | null

}

Запись данных


Запрос findMany используется для чтения данных из БД. Для записи данных используется запрос create:

async function main() {

// Создаем пост

const post = await prisma.post.create ({

data: {

title: 'Prisma облегчает работу с БД',

author: {

// Подключаем пост к записи в таблице `User`

connect: { email: 'sarah@prisma.io' }

}

}

})

console.log (post)

const allUsers = await prisma.user.findMany ({

include: { posts: true }

})

console.dir (allUsers, { depth: null })

}

Запускаем код:

yarn dev

# или

npm run dev

Вывод:

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: false,

authorId: 1

}

[

{

id: 1,

email: 'sarah@prisma.io',

name: 'Sarah',

posts: [

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: false,

authorId: 1

}

]

},

{

id: 2,

email: 'maria@prisma.io',

name: 'Maria',

posts: [

{

id: 1,

title: 'Hello World',

content: null,

published: false,

authorId: 2

}

]

}

]

Наш запрос добавил новую запись в таблицу Post:


Давайте «опубликуем» созданный пост с помощью запроса update:

async function main() {

const post = await prisma.post.update ({

where: { id: 2 },

data: { published: true }

})

console.log (post)

}

Запускаем код:

yarn dev

# или

npm run dev

Вывод:

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: true,

authorId: 1

}

Наш запрос обновил соответствующую запись в таблице Post:


Изменение схемы


Сначала добавим новую модель (Profile) в нашу схему:

model Post {

id Int @id @default (autoincrement ())

title String

content String?

published Boolean @default (false)

author User? @relation (fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default (autoincrement ())

email String @unique

name String?

posts Post[]

profile Profile?

}

model Profile {

id Int @id @default (autoincrement ())

bio String

user User @relation (fields: [userId], references: [id])

userId Int @unique

}

Обратите внимание, что мы также добавили новое поле в модель User.

Затем выполняем следующую команду:

npx prisma migrate dev --name add-profile

После этого у нас появляется возможность запрашивать данные из таблицы Profile, например, с помощью prisma.profile.findMany().

Express REST API


В данном разделе мы с нуля реализуем REST API с помощью Prisma Client, Express и TypeScript.

REST (от англ. Representational State Transfer — «передача состояния представления») — архитектурный стиль взаимодействия компонентов распределенного приложения в сети.

Инициализация проекта и установка зависимостей


Создаем директорию проекта, инициализируем проект и устанавливаем зависимости:

# создание директории

mkdir prisma-express

cd!$

# инициализация проекта

yarn init -yp

# или

npm init -y

# установка основных зависимостей

yarn add @prisma/client express

# или

npm i @prisma/client express

# установка зависимостей для разработки

yarn add -D prisma typescript ts-node @types/express @types/node

# или

npm i -D prisma typescript ts-node @types/express @types/node

Структура нашего проекта будет следующей:

- prisma

— schema.prisma — схема и модели Prisma

— seed.ts — скрипт для наполнения БД фиктивными данными

— index.ts — основной файл приложения

— package.json

— tscongig.json — настройки TypeScript

Содержание package.json:

{

«name»: «prisma-express»,

«version»:»1.0.0»,

«license»: «MIT»,

«scripts»: {

«dev»: «ts-node src/index.ts»

},

«dependencies»: {

»@prisma/client»:»2.21.2»,

«express»:»4.17.1»

},

«devDependencies»: {

«prisma»:»2.21.2»,

»@types/express»:»4.17.11»,

»@types/node»:»12.20.10»,

«ts-node»:»9.1.1»,

«typescript»:»4.2.4»

}

}

Содержание tsconfig.json:

{

«compilerOptions»: {

«sourceMap»: true,

«outDir»: «dist»,

«strict»: true,

«lib»: [«esnext»],

«esModuleInterop»: true

}

}

Схема и модели


Наша схема будет состоять из 2 моделей — User и Post:

// prisma/schema.prisma

generator client {

provider = «prisma-client-js»

}

datasource db {

provider = «sqlite»

url = «file:./dev.db»

}

model User {

id Int @id @default (autoincrement ())

email String @unique

name String?

posts Post[]

}

model Post {

id Int @id @default (autoincrement ())

createdAt DateTime @default (now ())

updatedAt DateTime @updatedAt

title String

content String?

published Boolean @default (false)

viewCount Int @default (0)

author User? @relation (fields: [authorId], references: [id])

authorId Int?

}

Как видите, мы снова испольуем SQLite в качестве БД, модель User не изменилась, а у модели Post появилось несколько дополнительных полей.

Файл prisma/seed.ts будет использоваться для наполнения БД фиктивными данными. Вставьте в него следующий код:

import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient ()

const userData: Prisma.UserCreateInput[] = [

{

name: 'John',

email: 'john@mail.com',

posts: {

create: [

{

title: 'Title1',

content: 'Some text',

published: true,

},

],

},

},

{

name: 'Jane',

email: 'jane@mail.com',

posts: {

create: [

{

title: 'Title2',

content: 'Another text',

published: true,

},

],

},

},

{

name: 'Alice',

email: 'alice@mail.com',

posts: {

create: [

{

title: 'Title3',

content: 'And another',

published: true,

},

{

title: 'Title4',

content: 'And another once again',

},

],

},

},

]

async function main () {

console.log (`Наполнение БД фиктивными данными…`)

for (const u of userData) {

const user = await prisma.user.create ({

data: u,

})

console.log (`Пользователь с id ${user.id} успешно создан`)

}

console.log (`Наполнение БД данными закончено.`)

}

main ()

.catch ((e) => {

console.error (e)

process.exit (1)

})

.finally (async () => {

await prisma.$disconnect ()

})

В данном скрипте мы просто перебираем массив userData и создаем пользователей с помощью запроса create.

Прежде чем мы перейдем к непосредственной реализации REST API, создадим БД и наполним ее данными.

Для создания БД выполняем следующую команду:

npx prisma migrate dev --name init

03119ead9e2ec073e07ab8713d018799.png

Запускам выполнение скрипта из prisma/seed.ts:

npx prisma db seed --preview-feature

da684c67e11c93bcd205bae17e196d9b.png

Теперь наша БД готова к использованию.

REST API


Реализуем минимальный сервер с помощью Express:

// index.ts

// Импортируем Express

import express from 'express'

// Создаем экземпляр приложения

const app = express ()

// Подключаем посредника (middleware) для разбора JSON и помещение данных в req.body

app.use (express.json ())

// Запускаем сервер на порту 3000

app.listen (3000, () => console.log (` Сервер запущен по адресу: http://localhost:3000`))

Далее импортируем Prisma Client и создаем экземпляр клиента:

import express from 'express'

// Импортируем Prisma Client и типы для TypeScript

import { Prisma, PrismaClient } from '@prisma/client'

const app = express ()

// Создаем экземпляр клиента

const prisma = new PrismaClient ()

Теперь определимся с конечными точками (endpoints), которые будут нужны нашему приложению:

`GET`

— `/users`: получение всех пользователей

— `/post/: id`: получение поста по его `id`

— `/user/: id/drafts`: получение черновиков (неопубликованных постов, постов, значением свойства `published` которых является `false`) определенного пользователя

— `/feed? searchString={searchString}&take={take}&skip={skip}&orderBy={orderBy}`: получение всех опубликованных постов

— параметры строки запроса (все параметры являются опциональными):

— `searchString`: фильтрация постов по заголовку (`title`) или содержанию (`content`)

— `take`: количество возвращаемых объектов

— `skip`: количество объектов, которые должны быть пропущены

— `orderBy`: порядок сортировки — по возврастанию или по убыванию. Возможные значения: `asc` или `desc`

`POST`

— `/post`: создание нового поста

— тело запроса:

— `title: String` — заголовок поста (обязательно)

— `content: String` — содержание поста (опционально)

— `authorEmail: String`: адрес электронной почты автора поста (обязательно)

— `/signup`: создание нового пользователя

— тело запроса:

— `email: String`: email пользователя (обязательно)

— `name: String`: имя пользователя (опционально)

— `postData: PostCreateInput[]`: посты, принадлежащие пользователю (опционально)

`PUT`

— `publish/: id`: публикация определенного поста (установка его свойства `published` в значение `true`)

— `/post/: id/views`: увеличение количества просмотров определенного поста

`DELETE`

— `/post/: id`: удаление определенного поста

Начнем с GET-запросов.

Получение всех пользователей:

app.get('/users', async (req, res) => {

// Для получение всех объектов определенной модели используется запрос `findMany ()`

const users = await prisma.user.findMany ()

// Возвращаем ответ в формате JSON

res.json (users)

})

Получение определенного поста:

app.get(`/post/:id`, async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// Для получения определенного (уникального) объекта модели используется запрос `findUnique ()`

// с настройкой `where` (где)

// Обратите внимание, что `id` имеет тип `Int` (число),

// поэтому мы выполняем преобразование типа с помощью фукнции `Number ()`

const post = await prisma.post.findUnique ({

where: { id: Number (id) },

})

res.json (post)

})

Получение черновиков пользователя:

app.get('/user/:id/drafts', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// В данном случае логика немного сложнее:

// сначала необходимо получить все посты определенного пользователя,

// затем их отфильтровать

const drafts = await prisma.user.findUnique ({

where: {

id: Number (id),

}

}).posts ({

where: { published: false }

})

res.json (drafts)

})

Получение всех опубликованных постов с условиями:

app.get('/feed', async (req, res) => {

// Извлекаем параметры из строки запроса

const { searchString, skip, take, orderBy } = req.query

// Формируем строку поиска

const or: Prisma.PostWhereInput = searchString? {

OR: [

{ title: { contains: searchString as string } },

{ content: { contains: searchString as string } },

],

} : {}

// Получаем все опубликованные посты с применением ограничений, если таковые имеются

// Сортировка выполняется по дате и времени последнего обновления

const posts = await prisma.post.findMany ({

where: {

published: true,

…or

},

include: { author: true },

take: Number (take) || undefined,

skip: Number (skip) || undefined,

orderBy: {

updatedAt: orderBy as Prisma.SortOrder

},

})

res.json (posts)

})

POST-запросы.

Создание нового поста:

app.post(`/post`, async (req, res) => {

// Извлекаем данные из тела запроса

const { title, content, authorEmail } = req.body

// Создаем новый пост с помощью запроса `create`

// Обратите внимание, что мы подключаем пост к таблице `User`

// с привязкой к `email` автора поста с помощью настройки `connect`

const result = await prisma.post.create ({

data: {

title,

content,

author: { connect: { email: authorEmail } },

},

})

res.json (result)

})

Создание нового пользователя:

app.post(`/signup`, async (req, res) => {

// Извлекаем данные из тела запроса

const { name, email, posts } = req.body

// У пользователя может быть несколько постов

const postData = posts?.map ((post: Prisma.PostCreateInput) => {

return { title: post?.title, content: post?.content }

})

// Обратите внимание, что мы используем запрос `create` дважды:

// один раз для создания пользователя и еще один для создания постов

const result = await prisma.user.create ({

data: {

name,

email,

posts: {

create: postData

}

},

})

res.json (result)

})

PUT-запросы.

Публикация поста:

app.put('/publish/:id', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

try {

// Находим пост с указанным `id`

// Нас интересует только поле `published` -

// делаем выборку с помощью настройки `select`

const postData = await prisma.post.findUnique ({

where: { id: Number (id) },

select: {

published: true

}

})

// Меняем значение свойства `published` на противоположное

// с помощью запроса `update`

const updatedPost = await prisma.post.update ({

where: { id: Number (id) },

data: { published: ! postData?.published },

})

res.json (updatedPost)

} catch (error) {

// Если в процессе обработки запроса возникла ошибка, то, скорее всего, поста с указанным `id` не существует

res.json ({ error: `Пост с указанным id ${id} не найден` })

}

})

Увеличение количества просмотров поста:

app.put('/post/:id/views', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

try {

// Увеличиваем значение свойства `viewCount` с помощью операции `increment: 1`, где

// `increment` означает увеличение, а `1` — количество, на которое происходит увеличение

const post = await prisma.post.update ({

where: { id: Number (id) },

data: {

viewCount: {

increment: 1

}

}

})

res.json (post)

} catch (error) {

// Если в процессе обработки запроса возникла ошибка, то, скорее всего, поста с указанным `id` не существует

res.json ({ error: `Пост с указанным id ${id} не найден` })

}

})

DELETE-запрос.

Удаление поста:

app.delete(`/post/:id`, async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// Удаляем пост с помощью запроса `delete`

const post = await prisma.post.delete ({

where: {

id: Number (id),

},

})

res.json (post)

})

На этом разработка нашего REST API завершена.

С другими примерами использования Prisma можно ознакомиться здесь.

Проверка работоспособности API


Пришло время убедиться в работоспособности нашего REST API. Для этого воспользуемся Postman. Обратите внимание, что для работы с localhost необходимо установить настольного агента (desktop agent).

Запускаем сервер с помощью команды:

yarn dev

# или

npm run dev

e637aa14661df2af5d82059c728d8cd1.png

Получаем всех пользователей:
85b5254d258d33ba0279c8b90675755f.png

Получаем пост с id === 2:
7bd1a5caec6ffe5c0a8cb882234c4009.png

Создаем нового пользователя:
5227fe7ae98ac3943f1a784bc9d58967.png

Создаем новый черновик от лица Боба:
d0ffe653da8417d0b787c0ba44795a7e.png

Публикуем данный черновик:
1a596c6cb2f2a8963d4bf2f629faa331.png

Увеличиваем количество его просмотров:
bb7c3a1866c621a1677eabd5d8bde23c.png

Полагаю, мы убедились в том, что наш сервис прекрасно функционирует.

Заключение


Итак, какие выводы можно сделать из проведенного нами обзора Prisma ORM? Безусловно, по сравнению с другими популярными решениями для работы с БД семейства SQL, такими как Sequelize или TypeORM, Prisma выглядит более привлекательно как с точки зрения удобства создания и изменения БД, так и с точки зрения простоты формирования запросов и получения данных.

Если же говорить о более специализированных инструментах, таких как Mongoose, то сложно вынести окончательный вердикт, учитывая, что разработчики Prisma обещают в ближайшее время представить MongoDB connector. Однако, если на данный момент Prisma и уступает Mongoose в некоторых аспектах, тот факт, что Prisma умеет работать с несколькими реляционными БД, а также предоставляет возможность выполнять комплексные (include) и точечные (select) запросы (по аналогии с GraphQL), заставляет внимательно следить за ее дальнейшим развитием.

Вместе с тем, нельзя сказать, что Prisma реализует совершенно новый подход к работе с БД. Речь идет, скорее, о доработке существующих технологий, об их совершенствовании в русле доминирующих концепций.


Наши серверы можно использовать для разработки на WebAssembly.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

et1aypandyuamqprsz3m2ntm4ky.png

© Habrahabr.ru