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


В этой серии из 2 статей я хочу поделиться с вами своими заметками о Prisma.


Prisma — это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для Node.js и TypeScript. Проще говоря, Prisma — это инструмент, позволяющий работать с реляционными (PostgreSQL, MySQL, SQL Server, SQLite) и нереляционной (MongoDB) базами данных с помощью JavaScript или TypeScript без использования SQL (хотя такая возможность имеется).


Содержание этой части



Первая часть.


Если вам это интересно, прошу под кат.


Клиент


Настройки


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']
    }
  }
})

  • notInn не содержится в списке;
  • ltn меньше x

const notPopularPosts = await prisma.post.findMany({
  where: {
    likeCount: {
      lt: 100
    }
  }
})

  • lten меньше или равно x;
  • gtn больше x;
  • gten больше или равно x;
  • containsn содержит x

const admins = await prisma.user.findMany({
  where: {
    email: {
      contains: 'admin'
    }
  }
})

  • startsWithn начинается с x

const usersWithNameStartsWithA = await prisma.user.findMany({
  where: {
    user_name: {
      startsWith: 'A'
    }
  }
})

  • endsWithn заканчивается 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!




Комментарии (1)


  1. jesaiah4
    09.03.2022 13:16

    это больше как реклама своего сервиса чтоли? т.к. комменты в ч1 вообще мертвые