Prisma
- это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) дляNode.js
иTypeScript
. Проще говоря,Prisma
- это инструмент, позволяющий работать с реляционными (PostgreSQL
,MySQL
,SQL Server
,SQLite
) и нереляционной (MongoDB
) базами данных с помощьюJavaScript
илиTypeScript
без использованияSQL
(хотя такая возможность имеется).
- Инициализация проекта
- CLI
- Схема
- Клиент
Создаем директорию, переходим в нее и инициализируем Node.js-проект
:
mkdir prisma-test
cd prisma-test
yarn init -yp
# or
npm init -y
Устанавливаем Prisma
в качестве зависимости для разработки:
yarn add -D prisma
# or
npm i -D prisma
Инициализируем проект Prisma
:
npx prisma init
Это приводит к генерации файлов prisma/schema.prisma
и .env
.
В файле .env
содержится переменная DATABASE_URL
, значением которой является путь к (адрес) БД. Файл schema.prisma
мы рассмотрим позже.
Интерфейс командной строки (Command line interface, CLI) Prisma
предоставляет следующие основные возможности (команды):
init
- создает шаблонPrisma-проекта
:--datasource-provider
- провайдер для работы с БД:sqlite
,postgresql
,mysql
,sqlserver
илиmongodb
(перезаписываетdatasource
изschema.prisma
);--url
- адрес БД (перезаписываетDATABASE_URL
)
npx prisma init --datasource-provider mysql --url mysql://user:password@localhost:3306/mydb
generate
- генерирует клиентаPrisma
на основе схемы (schema.prisma
). КлиентPrisma
предоставляет программный интерфейс приложения (Application Programming Interface, API) для работы с моделями и типы дляTypeScript
npx prisma generate
db
pull
- генерирует модели на основе существующей схемы БД
npx prisma db pull
push
- синхронизирует состояние схемыPrisma
с БД без выполнения миграций. БД создается при отсутствии. Используется для прототипировании БД и в локальной разработке. Также может быть полезной в случае ограниченного доступа к БД, например, при использовании БД, предоставляемой облачными провайдерами, такими какElephantSQL
илиHeroku
npx prisma db push
seed
- выполняет скрипт для наполнения БД начальными (фиктивными) данными. Путь к соответствующему файлу определяется вpackage.json
"prisma": {
"seed": "node prisma/seed.js"
}
npx prisma seed
migrate
dev
- выполняет миграцию для разработки:--name
- название миграции
npx prisma migrate dev --name init
Это приводит к созданию БД при ее отсутствии, генерации файла prisma/migrations/migration_name.sql
, выполнению инструкции из этого файла (синхронизации БД со схемой) и генерации (регенерации) клиента (prisma generate
).
Данная команда должна выполняться после каждого изменения схемы.
reset
- удаляет и заново создает БД или выполняет "мягкий сброс", удаляя все данные, таблицы, индексы и другие артефакты
npx prisma migrate reset
deploy
- выполняет производственную миграцию
npx prisma migrate deploy
studio
- позволяет просматривать и управлять данными, хранящимися в БД, в интерактивном режиме:--browser
,-b
- название браузера (по умолчанию используется дефолтный браузер);--port
,-p
- номер порта (по умолчанию -5555
)
npx prisma studio
# без автоматического открытия вкладки браузера
npx prisma studio -b none
Подробнее о CLI
можно почитать здесь.
В файле schema.prisma
мы видим такие строки:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
datasource
- источник данных:provider
- название провайдера для доступа к БД:sqlite
,postgresql
,mysql
,sqlserver
илиmongodb
(по умолчанию -postgresql
);url
- адрес БД (по умолчанию - значение переменнойDATABASE_URL
);shadowDatabaseUrl
- адрес "теневой" БД (для БД, предоставляемых облачными провайдерами): используется для миграций для разработки (prisma migrate dev
);
generator
- генератор клиента на основе схемы:provider
- провайдер генератора (единственным доступным на сегодняшний день провайдером являетсяprisma-client-js
);binaryTargets
- определяет операционную систему для клиентаPrisma
. Значением по умолчанию являетсяnative
, но иногда это приходится указывать явно, например, при использовании клиента вDocker-контейнере
(в этом случае также приходится явно выполнятьprisma generate
)
generator client {
provider = "prisma-client-js"
binaryTargets = ["native"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
Для работы со схемой удобно пользоваться расширением Prisma
для VSCode
. Соответствующий раздел в файле settings.json
должен выглядеть так:
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
Определим в схеме модели для пользователя (User
) и поста (Post
):
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
hash String @map("password_hash")
first_name String?
last_name String?
age Int?
role Role @default(USER)
posts Post[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@map("users")
}
model Post {
id String @id @default(uuid())
title String
content String
published Boolean
author_id String
author User @relation(fields: [author_id], references: [id])
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@map("posts")
}
enum Role {
USER
ADMIN
}
Вот что мы здесь видим:
id
,email
,hash
etc. - названия полей (колонок таблицы);@map
привязывает поле схемы (hash
) к указанной колонке таблицы (password_hash
).@map
не меняет название колонки в БД и поля в генерируемом клиенте. ДляMongoDB
использование@map
для@id
является обязательным:id String @default(auto()) @map("_id") @db.ObjectId
;String
,Int
,DateTime
etc. - типы данных (см. ниже);@db.Uuid
- тип данных, специфичный для одной или нескольких БД (в данном случаеPostgreSQL
);- модификатор
?
после названия типа означает, что данное поле является опциональным (необязательным, может иметь значениеNULL
); - модификатор
[]
после названия типа означает, что значением данного поля является список (массив). Такое поле не может быть опциональным; - префикс
@
означает атрибут поля, а префикс@@
- атрибут блока (модели, таблицы). Некоторые атрибуты принимают параметры; - атрибут
@id
означает, что данное поле является первичным (основным) ключом таблицы (PRIMARY KEY
) (идентификатор модели). Такое поле не может быть опциональным; - атрибут
@default
присваивает полю указанное значение по умолчанию (при отсутствии значения поля) (DEFAULT
). Дефолтными могут быть статические значения (42
,hi
) или значения, генерируемые функциямиautoincrement
,dbgenerated
,cuid
,uuid
иnow
(функции атрибутов; см. ниже); - атрибут
@unique
означает, что значение поля должно быть уникальным в пределах таблицы (UNIQUE
). Таблица должна иметь хотя бы одно поле@id
или@unique
; - атрибут
@relation
указывает на существование отношений между таблицами. В данном случае между таблицамиusers
иposts
существуют отношения один-ко-многим (one-to-many, 1-n) - у одного пользователя может быть несколько постов (FOREIGN KEY / REFERENCES
) (об отношениях мы поговорим отдельно); - атрибут
@updatedAt
обновляет поле текущими датой и временем при любой модификации записи; - у нас имеется перечисление (enum), значения которого используются в качестве значений поля
role
моделиUser
(значением по умолчанию являетсяUSER
); - атрибут
@@map
привязывает название модели к названию таблицы в БД.@@map
не меняет название таблицы в БД и модели в генерируемом клиенте.
Допустимыми в названиях полей являются следующие символы: [A-Za-z][A-Za-z0-9_]*
.
String
- строка переменной длины (дляPostgreSQL
- это типtext
);Boolean
- логическое значение:true
илиfalse
(boolean
);Int
- целое число (integer
);BigInt
-BigInt
(integer
);Float
- число с плавающей точкой (запятой) (double precision
);Decimal
(decimal(65,30)
);DateTime
- дата и время в форматеISO 8601
;Json
- объект в форматеJSON
(jsonb
);Bytes
(bytea
).
Атрибут @db
позволяет использовать типы данных, специфичные для одной или нескольких БД.
Кроме упомянутых выше, в схеме можно использовать следующие атрибуты:
@@id
- определяет составной (composite) первичный ключ таблицы, например,@@id[title, author]
(в данном случае соответствующее поле будет называтьсяtitle_author
- это можно изменить);@@unique
- определяет составное ограничение уникальности (unique constraint) для указанных полей (такие поля не могут быть опциональными), например,@@unique([title, author])
;@@index
- определяет индекс в БД (INDEX
), например,@@index([title, author])
;@ignore
,@@ignore
- используется для обозначения невалидных полей и моделей, соответственно.
auto
- представляет дефолтные значения, генерируемые БД (только дляMongoDB
);autoincrement
- генерирует последовательные целые числа (SERIAL
вPostgreSQL
, не поддерживаетсяMongoDB
);cuid
- генерирует глобальный уникальный идентификатор на основе спецификацииcuid
;uuid
- генерирует глобальный уникальный идентификатор на основе спецификацииUUID
;now
- возвращает текущую отметку времени (timestamp) (CURRENT_TIMESTAMP
вPostgreSQL
);dbgenerated
- представляет дефолтные значения, которые не могут быть выражены в схеме (например,random()
).
Подробнее о схеме можно почитать здесь.
Атрибут @relation
указывает на существование отношений между моделями (таблицами). Он принимает следующие параметры:
name?: string
- название отношения;fields?: [field1, field2, ...fieldN]
- список полей текущей модели (в нашем случае это[author_id]
моделиPost
); обратите внимание: само поле определяется отдельно);references: [field1, field2, ...fieldN]
- список полей другой модели (стороны отношений) (в нашем случае это[id]
моделиUser
).
В приведенной выше схеме полями, указывающими на существование отношений между моделями User
и Post
, являются поля posts
и author
. Эти поля существуют только на уровне Prisma
, в БД они не создаются. Скалярное поле author_id
также существует только на уровне Prisma
- это внешний ключ (FOREIGN KEY
), соединяющий Post
с User
.
Как известно, существует 3 вида отношений:
- один-к-одному (one-to-one, 1-1);
- один-ко-многим (one-to-many, 1-n);
- многие-ко-многим (many-to-many, m-n).
Атрибут @relation
является обязательным только для отношений 1-1
и 1-n
.
Предположим, что в нашей схеме имеются такие модели:
model User {
id Int @id @default(autoincrement())
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
Вот что мы здесь видим:
- между моделями
User
иProfile
существуют отношения1-1
- у одного пользователя может быть только один профиль; - между моделями
User
иPost
существуют отношения1-n
- у одного пользователя может быть несколько постов; - между моделями
Post
иCategory
существуют отношенияm-n
- один пост может принадлежать к нескольким категориям, в одну категорию может входить несколько постов.
Подробнее об отношениях можно почитать здесь.
Импортируем и создаем экземпляр клиента Prisma
:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma
Иногда может потребоваться делать так:
const Prisma = require('prisma')
const prisma = new Prisma.PrismaClient()
module.exports = prisma
findUnique
позволяет извлекать единичные записи по идентификатору или уникальному полю.
Сигнатура
findUnique({
where: condition,
select?: fields,
include?: relations,
rejectOnNotFound?: boolean
})
Модификатор ?
означает, что поле является опциональным.
condition
- условие для выборки;fields
- поля для выборки;relations
- отношения (связанные поля) для выборки;rejectOnNotFound
- если имеет значениеtrue
, при отсутствии записи выбрасывается исключениеNotFoundError
. Если имеет значениеfalse
, при отсутствии записи возвращаетсяnull
.
Пример
async function getUserById(id) {
try {
const user = await prisma.user.findUnique({
where: {
id
}
})
return user
} catch(e) {
onError(e)
}
}
findFirst
возвращает первую запись, соответствующую заданному критерию.
Сигнатура
findFirst({
where?: condition,
select?: fields,
include?: relations,
rejectOnNotFound?: boolean,
distinct?: field,
orderBy?: order,
cursor?: position,
skip?: number,
take?: number
})
distinct
- фильтрация по определенному полю;orderBy
- сортировка по определенному полю и в определенном порядке;cursor
- позиция начала списка (как правило,id
или другое уникальное значение);skip
- количество пропускаемых записей;take
- количество возвращаемых записей (в данном случае может иметь значение1
или-1
: во втором случае возвращается последняя запись.
Пример
async function getLastPostByAuthorId(author_id) {
try {
const post = await prisma.post.findFirst({
where: {
author_id
},
orderBy: {
created_at: 'asc'
},
take: -1
})
return post
} catch(e) {
onError(e)
}
}
findMany
возвращает все записи, соответствующие заданному критерию.
Сигнатура
findMany({
where?: condition,
select?: fields,
include?: relations,
rejectOnNotFound?: boolean,
distinct?: field,
orderBy?: order,
cursor?: position,
skip?: number,
take?: number
})
Пример
async function getAllPostsByAuthorId(author_id) {
try {
const posts = await prisma.post.findMany({
where: {
author_id
},
orderBy: {
updated_at: 'desc'
}
})
return posts
} catch(e) {
onError(e)
}
}
create
создает новую запись.
Сигнатура
create({
data: _data,
select?: fields,
include?: relations
})
_data
- данные создаваемой записи.
Пример
async function createUserWithProfile(data) {
const { email, password, firstName, lastName, age } = data
try {
const hash = await argon2.hash(password)
const user = await prisma.user.create({
data: {
email,
hash,
profile: {
create: {
first_name: firstName,
last_name: lastName,
age
}
}
},
select: {
email: true
},
include: {
profile: true
}
})
return user
} catch(e) {
onError(e)
}
}
update
обновляет существующую запись.
Сигнатура
update({
data: _data,
where: condition,
select?: fields,
include?: relations
})
Пример
async function updateUserById(id, changes) {
const { email, age } = changes
try {
const user = await prisma.user.update({
where: {
id
},
data: {
email,
profile: {
update: {
age
}
}
},
select: {
email: true
},
include: {
profile: true
}
})
return user
} catch(e) {
onError(e)
}
}
upsert
обновляет существующую или создает новую запись.
Сигнатура
upsert({
create: _data,
update: _data,
where: condition,
select?: fields,
include?: relations
})
Пример
async function updateOrCreateUser(data) {
const { userName, email, password } = data
try {
const hash = await argon2.hash(password)
const user = await prisma.user.create({
where: { user_name: userName },
update: {
email,
hash
},
create: {
email,
hash,
user_name: userName
},
select: { user_name: true, email: true }
})
return user
} catch(e) {
onError(e)
}
}
delete
удаляет существующую запись по идентификатору или уникальному полю.
Сигнатура
delete({
where: condition,
select?: fields,
include?: relations
})
Пример
async function removeUserById(id) {
try {
await prisma.user.delete({
where: {
id
}
})
} catch(e) {
onError(e)
}
}
createMany
создает несколько записей с помощью одной транзакции (о транзакциях мы поговорим отдельно).
Пример
createMany({
data: _data[],
skipDuplicates?: boolean
})
_data[]
- данные для создаваемых записей в виде массива;skipDuplicates
- при значенииtrue
создаются только уникальные записи.
Пример
// предположим, что `users` - это массив объектов
async function createUsers(users) {
try {
const users = await prisma.user.createMany({
data: users
})
return users
} catch(e) {
onError(e)
}
}
updateMany
обновляет несколько существующих записей за один раз и возвращает количество (sic) обновленных записей.
Сигнатура
updateMany({
data: _data[],
where?: condition
})
Пример
async function updateProductsByCategory(category, newDiscount) {
try {
const count = await prisma.product.updateMany({
where: {
category
},
data: {
discount: newDiscount
}
})
return count
} catch(e) {
onError(e)
}
}
deleteMany
удаляет несколько записей с помощью одной транзакции и возвращает количество удаленных записей.
Сигнатура
deleteMany({
where?: condition
})
Пример
async function removeAllPostsByUserId(author_id) {
try {
const count = await prisma.post.deleteMany({
where: {
author_id
}
})
return count
} catch(e) {
onError(e)
}
}
count
возвращает количество записей, соответствующих заданному критерию.
Сигнатура
count({
where?: condition,
select?: fields,
cursor?: position,
orderBy?: order,
skip?: number,
take?: number
})
Пример
async function countUsersWithPublishedPosts() {
try {
const count = await prisma.user.count({
where: {
post: {
some: {
published: true
}
}
}
})
return count
} catch(e) {
onError(e)
}
}
aggregate
выполняет агрегирование полей.
Сигнатура
aggregate({
where?: condition,
select?: fields,
cursor?: position,
orderBy?: order,
skip?: number,
take?: number,
_count: count,
_avg: avg,
_sum: sum,
_min: min,
_max: max
})
_count
- возвращает количество совпадающих записей или неnull-полей
;_avg
- возвращает среднее значение определенного поля;_sum
- возвращает сумму значений определенного поля;_min
- возвращает наименьшее значение определенного поля;_max
- возвращает наибольшее значение определенного поля.
Пример
async function getAllUsersCountAndMinMaxProfileViews() {
try {
const result = await prisma.user.aggregate({
_count: {
_all: true
},
_max: {
profileViews: true
},
_min: {
profileViews: true
}
})
return result
} catch(e) {
onError(e)
}
}
groupBy
выполняет группировку полей.
Сигнатура
groupBy({
by?: by,
having?: having,
where?: condition,
orderBy?: order,
skip?: number,
take?: number,
_count: count,
_avg: avg,
_sum: sum,
_min: min,
_max: max
})
by
- определяет поле или комбинацию полей для группировки записей;having
- позволяет фильтровать группы по агрегируемому значению.
Пример
В следующем примере мы выполняем группировку по country / city
, где среднее значение profileViews
превышает 100
, и возвращаем общее количество (_sum
) profileViews
для каждой группы. Запрос также возвращает количество всех (_all
) записей в каждой группе и все записи с не null
значениями поля city
в каждой группе:
async function getUsers() {
try {
const result = await prisma.user.groupBy({
by: ['country', 'city'],
_count: {
_all: true,
city: true
},
_sum: {
profileViews: true
},
orderBy: {
country: 'desc'
},
having: {
profileViews: {
_avg: {
gt: 100
}
}
}
})
return result
} catch(e) {
onError(e)
}
}
select
определяет, какие поля включаются в возвращаемый объект.
const user = await prisma.user.findUnique({
where: { email },
select: {
id: true,
email: true,
first_name: true,
last_name: true,
age: true
}
})
// or
const usersWithPosts = await prisma.user.findMany({
select: {
id: true,
email: true,
posts: {
select: {
id: true,
title: true,
content: true,
author_id: true,
created_at: true
}
}
}
})
// or
const usersWithPostsAndComments = await prisma.user.findMany({
select: {
id: true,
email: true,
posts: {
include: {
comments: true
}
}
}
})
include
определяет, какие отношения (связанные записи) включаются в возвращаемый объект.
const userWithPostsAndComments = await prisma.user.findUnique({
where: { email },
include: {
posts: true,
comments: true
}
})
where
определяет один или более фильтр (о фильтрах мы поговорим отдельно), применяемый к свойствам записи или связанных записей:
const admins = await prisma.user.findMany({
where: {
email: {
contains: 'admin'
}
}
})
orderBy
определяет поля и порядок сортировки. Возможными значениями orderBy
являются asc
и desc
.
const usersByPostCount = await prisma.user.findMany({
orderBy: {
posts: {
count: 'desc'
}
}
})
distinct
определяет поля, которые должны быть уникальными в возвращаемом объекте.
const distinctCities = await prisma.user.findMany({
select: {
city: true,
country: true
},
distinct: ['city']
})
create: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- добавляет новую связанную запись или набор записей в родительскую запись.create
доступен при создании (create
) новой родительской записи или обновлении (update
) существующей родительской записи
const user = await prisma.user.create({
data: {
email,
profile: {
// вложенный запрос
create: {
first_name,
last_name
}
}
}
})
createMany: [{ data1 }, { data2 }, ...{ dataN }]
- добавляет набор новых связанных записей в родительскую запись.createMany
доступен при создании (create
) новой родительской записи или обновлении (update
) существующей родительской записи
const userWithPosts = await prisma.user.create({
data: {
email,
posts: {
// !
createMany: {
data: posts
}
}
}
})
update: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- обновляет одну или более связанных записей
const user = await prisma.user.update({
where: { email },
data: {
profile: {
// !
update: { age }
}
}
})
updateMany: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- обновляет массив связанных записей. Поддерживается фильтрация
const result = await prisma.user.update({
where: { id },
data: {
posts: {
// !
updateMany: {
where: {
published: false
},
data: {
like_count: 0
}
}
}
}
})
upsert: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- обновляет существующую связанную запись или создает новую
const user = await prisma.user.update({
where: { email },
data: {
profile: {
// !
upsert: {
create: { age },
update: { age }
}
}
}
})
delete: boolean | { data } | [{ data1 }, { data2 }, ...{ dataN }]
- удаляет связанную запись. Родительская запись при этом не удаляется
const user = await prisma.user.update({
where: { email },
data: {
profile: {
delete: true
}
}
})
deleteMany: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- удаляет связанные записи. Поддерживается фильтрация
const user = await prisma.user.update({
where: { id },
data: {
age,
posts: {
// !
deleteMany: {}
}
}
})
set: { data } | [{ data1 }, { data2 }, ...{ dataN }]
- перезаписывает значение связанной записи
const userWithPosts = await prisma.user.update({
where: { email },
data: {
posts: {
// !
set: newPosts
}
}
})
connect
- подключает запись к существующей связанной записи по идентификатору или уникальному полю
const user = await prisma.post.create({
data: {
title,
content,
author: {
connect: { email }
}
}
})
connectOrCreate
- подключает запись к существующей связанной записи по идентификатору или уникальному полю либо создает связанную запись при отсутствии таковой;disconnect
- отключает родительскую запись от связанной без удаления последней.disconnect
доступен только если отношение является опциональным.
equals
- значение равняетсяn
const usersWithNameHarry = await prisma.user.findMany({
where: {
name: {
equals: 'Harry'
}
}
})
// `equals` может быть опущено
const usersWithNameHarry = await prisma.user.findMany({
where: {
name: 'Harry'
}
})
not
- значение не равняетсяn
;in
- значениеn
содержится в списке (массиве)
const usersWithNameAliceOrBob = await prisma.user.findMany({
where: {
user_name: {
// !
in: ['Alice', 'Bob']
}
}
})
notIn
-n
не содержится в списке;lt
-n
меньшеx
const notPopularPosts = await prisma.post.findMany({
where: {
likeCount: {
lt: 100
}
}
})
lte
-n
меньше или равноx
;gt
-n
большеx
;gte
-n
больше или равноx
;contains
-n
содержитx
const admins = await prisma.user.findMany({
where: {
email: {
contains: 'admin'
}
}
})
startsWith
-n
начинается сx
const usersWithNameStartsWithA = await prisma.user.findMany({
where: {
user_name: {
startsWith: 'A'
}
}
})
endsWith
-n
заканчиваетсяx
.
AND
- все условия должны возвращатьtrue
const notPublishedPostsAboutTypeScript = await prisma.post.findMany({
where: {
AND: [
{
title: {
contains: 'TypeScript'
}
},
{
published: false
}
]
}
})
Обратите внимание: оператор указывается до названия поля (снаружи поля), а фильтр после (внутри).
OR
- хотя бы одно условие должно возвращатьtrue
;NOT
- все условия должны возвращатьfalse
.
some
- возвращает все связанные записи, соответствующие одному или более критерию фильтрации
const usersWithPostsAboutTypeScript = await prisma.user.findMany({
where: {
posts: {
some: {
title: {
contains: 'TypeScript'
}
}
}
}
})
every
- возвращает все связанные записи, соответствующие всем критериям;none
- возвращает все связанные записи, не соответствующие ни одному критерию;is
- возвращает все связанные записи, соответствующие критерию;notIs
- возвращает все связанные записи, не соответствующие критерию.
$disconnect
- закрывает соединение с БД, которое было установлено после вызова метода$connect
(данный метод чаще всего не требуется вызывать явно), и останавливает движок запросов (query engine)Prisma
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function seedDb() {
try {
await prisma.model.create(data)
} catch (e) {
onError(e)
} finally {
// !
await prisma.$disconnect()
}
}
$use
- добавляет посредника (middleware)
prisma.$use(async (params, next) => {
console.log('Это посредник')
// работаем с `params`
return next(params)
})
-
next
- представляет "следующий уровень" в стеке посредников. Таким уровнем может быть следующий посредник или движок запросовPrisma
; -
params
- объект со следующими свойствами:action
- тип запроса, например,create
илиfindMany
;args
- аргументы, переданные в запрос, например,where
илиdata
;model
- модель, например,User
илиPost
;runInTransaction
- возвращаетtrue
, если запрос был запущен в контексте транзакции;
-
методы
$queryRaw
,$executeRaw
и$runCommandRaw
предназначены для работы сSQL
. Почитать о них можно здесь; -
$transaction
- выполняет запросы в контексте транзакции (см. ниже).
Подробнее о клиенте можно почитать здесь.
Транзакция - это последовательность операций чтения/записи, которые обрабатываются как единое целое, т.е. либо все операции завершаются успешно, либо все операции отклоняются с ошибкой.
Prisma
позволяет использовать транзакции тремя способами:
- вложенные запросы (см. выше): операции с родительскими и связанными записями выполняются в контексте одной транзакции
const newUserWithProfile = await prisma.user.create({
data: {
email,
profile: {
// !
create: {
first_name,
last_name
}
}
}
})
- пакетированные/массовые (batch/bulk) транзакции: выполнение нескольких операций за один раз с помощью таких запросов, как
createMany
,updateMany
иdeleteMany
const removedUser = await prisma.user.delete({
where: {
email
}
})
// !
await prisma.post.deleteMany({
where: {
author_id: removedUser.id
}
})
- вызов метода
$transaction
.
Интерфейс $transaction
может быть использован в двух формах:
$transaction([ query1, query2, ...queryN ])
- принимает массив последовательно выполняемых запросов;$transaction(fn)
- принимает функцию, которая может включать запросы и другой код.
Пример транзакции, возвращающей посты, в заголовке которых встречается слово TypeScript
и общее количество постов:
const [postsAboutTypeScript, totalPostCount] = await prisma.$transaction([
prisma.post.findMany({ where: { title: { contains: 'TypeScript' } } }),
prisma.post.count()
])
В $transaction
допускается использование SQL
:
const [userNames, updatedUser] = await prisma.$transaction([
prisma.$queryRaw`SELECT 'user_name' FROM users`,
prisma.$executeRaw`UPDATE users SET user_name = 'Harry' WHERE id = 42`
])
Интерактивные транзакции предоставляют разработчикам больший контроль над выполняемыми в контексте транзакции операциями. В данный момент они имеют статус экспериментальной возможности, которую можно включить следующим образом:
generator client {
provider = "prisma-client-js"
previewFeatures = ["interactiveTransactions"]
}
Рассмотрим пример совершения платежа.
Предположим, что у Alice
и Bob
имеется по 100$
на счетах (account), и Alice
хочет отправить Bob
свои 100$
.
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function transfer(from, to, amount) {
try {
await prisma.$transaction(async (prisma) => {
// 1. Уменьшаем баланс отправителя
const sender = await prisma.account.update({
data: {
balance: {
decrement: amount
}
},
where: {
email: from
}
})
// 2. Проверяем, что баланс отправителя после уменьшения >= 0
if (sender.balance < 0) {
throw new Error(`${from} имеет недостаточно средств для отправки ${amount}`)
}
// 3. Увеличиваем баланс получателя
const recipient = await prisma.account.update({
data: {
balance: {
increment: amount
}
},
where: {
email: to
}
})
return recipient
})
} catch(e) {
// обрабатываем ошибку
}
}
async function main() {
// эта транзакция разрешится
await transfer('alice@mail.com', 'bob@mail.com', 100)
// а эта провалится
await transfer('alice@mail.com', 'bob@mail.com', 100)
}
main().finally(() => {
prisma.$disconnect()
})
Подробнее о транзакциях можно почитать здесь.
The End.