Prisma ORM: полное руководство для начинающих (и не только). Часть 1

от автора

Привет, друзья!

В этой серии из 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, 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);
  • BigIntBigInt (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/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *