Привет, друзья!
В этой серии из 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
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!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/654341/

Добавить комментарий