Привет, друзья!
В этой серии из 2 статей я хочу поделиться с вами своими заметками о Prisma
.
Prisma
— это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для Node.js
и TypeScript
. Проще говоря, Prisma
— это инструмент, позволяющий работать с реляционными (PostgreSQL
, MySQL
, SQL Server
, SQLite
) и нереляционной (MongoDB
) базами данных с помощью JavaScript
или TypeScript
без использования SQL
(хотя такая возможность имеется).
Содержание этой части
- Настройки
- select
- include
- where
- orderBy
- distinct
- Вложенные запросы
- Фильтры и операторы
- Фильтры
- Операторы
- Фильтры для связанных записей
- Методы клиента
- Транзакции
- $transaction
- Интерактивные транзакции
Первая часть.
Если вам это интересно, прошу под кат.
Клиент
Настройки
select
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
include
определяет, какие отношения (связанные записи) включаются в возвращаемый объект.
const userWithPostsAndComments = await prisma.user.findUnique({
where: { email },
include: {
posts: true,
comments: true
}
})
where
where
определяет один или более фильтр (о фильтрах мы поговорим отдельно), применяемый к свойствам записи или связанных записей:
const admins = await prisma.user.findMany({
where: {
email: {
contains: 'admin'
}
}
})
orderBy
orderBy
определяет поля и порядок сортировки. Возможными значениями orderBy
являются asc
и desc
.
const usersByPostCount = await prisma.user.findMany({
orderBy: {
posts: {
count: 'desc'
}
}
})
distinct
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
может быть использован в двух формах:
$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()
})
Подробнее о транзакциях можно почитать здесь.
Пожалуй, это все, что я хотел рассказать вам о Prisma
.
Благодарю за внимание и happy coding!