
Привет, друзья!
В этой серии из 2 статей я хочу поделиться с вами своими заметками о Prisma.
Prisma — это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для Node.js и TypeScript. Проще говоря, Prisma — это инструмент, позволяющий работать с реляционными (PostgreSQL, MySQL, SQL Server, SQLite) и нереляционной (MongoDB) базами данных с помощью JavaScript или TypeScript без использования SQL (хотя такая возможность имеется).
Содержание этой части
Если вам это интересно, прошу под кат.
Инициализация проекта
Создаем директорию, переходим в нее и инициализируем 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 мы рассмотрим позже.
CLI
Интерфейс командной строки (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
-
db 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,hashetc. — названия полей (колонок таблицы); -
@mapпривязывает поле схемы (hash) к указанной колонке таблицы (password_hash).@mapне меняет название колонки в БД и поля в генерируемом клиенте. ДляMongoDBиспользование@mapдля@idявляется обязательным:id String @default(auto()) @map("_id") @db.ObjectId; -
String,Int,DateTimeetc. — типы данных (см. ниже); -
@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 позволяет извлекать единичные записи по идентификатору или уникальному полю.
Сигнатура
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 возвращает первую запись, соответствующую заданному критерию.
Сигнатура
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 возвращает все записи, соответствующие заданному критерию.
Сигнатура
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 создает новую запись.
Сигнатура
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 обновляет существующую запись.
Сигнатура
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 обновляет существующую или создает новую запись.
Сигнатура
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 удаляет существующую запись по идентификатору или уникальному полю.
Сигнатура
delete({
where: condition,
select?: fields,
include?: relations
})
Пример
async function removeUserById(id) {
try {
await prisma.user.delete({
where: {
id
}
})
} catch(e) {
onError(e)
}
}
createMany
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
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 удаляет несколько записей с помощью одной транзакции и возвращает количество удаленных записей.
Сигнатура
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 возвращает количество записей, соответствующих заданному критерию.
Сигнатура
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 выполняет агрегирование полей.
Сигнатура
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 выполняет группировку полей.
Сигнатура
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)
}
}
Пожалуй, это все, о чем я хотел рассказать вам в первой части.
Благодарю за внимание и happy coding!
Комментарии (4)

Ubudragon
04.03.2022 17:27+1а чем это принципиально лучше пакета typeorm ?

arthurlomakin
05.03.2022 10:28Ничем. Вы можете для Typeorm ещё и аналог linq сделать.
А Prisma непонятно зачем. Ещё и куча проблем со схемой

jesaiah4
05.03.2022 08:291)запросы прям mongodb
2) не увидел где тут шрафкл?
3) что такое аполо и причем он тут ?
4) по ссылке говорится о каком то апполо федерейшен, это что за зверь и как относится ко всем пунктам ?

PavelShek
Хорошая ORM, но столкнулись с тем, что нет возможности работать с несколькими схемами БД.