Это статья-обзор о 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
: файл БД SQLitescript.ts
: исполняемый скрипт TypeScripttsconfig.json
: настройки компилятора TypeScript
Зависимости проекта:
prisma
: Prisma CLI, который можно вызывать с помощьюnpx prisma
@prisma/client
: Prisma Client для доступа к БДtypescript
: набор инструментов TypeScriptts-node
: используется для запуска скрипта TypeScript
Файл
prisma/dev.db
содержит две таблицы с фиктивными данными:User | ||
---|---|---|
id | name | |
1 | sarah@prisma.io | Sarah |
2 | maria@prisma.io | Maria |
Post | ||||
---|---|---|---|---|
id | title | content | published | authorId |
1 | Hello World | null | false | 2 |
Обратите внимание: колонка
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
:Post | ||||
---|---|---|---|---|
id | title | content | published | authorId |
1 | Hello World | null | false | 2 |
2 | Prisma облегчает работу с БД | null | false | 1 |
Давайте «опубликуем» созданный пост с помощью запроса
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
:Post | ||||
---|---|---|---|---|
id | title | content | published | authorId |
1 | Hello World | null | false | 2 |
2 | Prisma облегчает работу с БД | null | true | 1 |
Изменение схемы
Сначала добавим новую модель (
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
Запускам выполнение скрипта из
prisma/seed.ts
:npx prisma db seed --preview-feature
Теперь наша БД готова к использованию.
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
Получаем всех пользователей:
Получаем пост с
id === 2
:Создаем нового пользователя:
Создаем новый черновик от лица Боба:
Публикуем данный черновик:
Увеличиваем количество его просмотров:
Полагаю, мы убедились в том, что наш сервис прекрасно функционирует.
Заключение
Итак, какие выводы можно сделать из проведенного нами обзора Prisma ORM? Безусловно, по сравнению с другими популярными решениями для работы с БД семейства SQL, такими как Sequelize или TypeORM, Prisma выглядит более привлекательно как с точки зрения удобства создания и изменения БД, так и с точки зрения простоты формирования запросов и получения данных.
Если же говорить о более специализированных инструментах, таких как Mongoose, то сложно вынести окончательный вердикт, учитывая, что разработчики Prisma обещают в ближайшее время представить MongoDB connector. Однако, если на данный момент Prisma и уступает Mongoose в некоторых аспектах, тот факт, что Prisma умеет работать с несколькими реляционными БД, а также предоставляет возможность выполнять комплексные (include) и точечные (select) запросы (по аналогии с GraphQL), заставляет внимательно следить за ее дальнейшим развитием.
Вместе с тем, нельзя сказать, что Prisma реализует совершенно новый подход к работе с БД. Речь идет, скорее, о доработке существующих технологий, об их совершенствовании в русле доминирующих концепций.
Облачные серверы от Маклауд можно использовать для разработки на WebAssembly.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
myrkoxx
Сделайте, пожалуйста, что-то с вставками кода