Обзор Prisma ORM — инструмента для работы с Node.js и TypeScript
В этой статье я хотел бы пройти по процессу установки и использованию Prisma ORM. У меня есть большой опыт использования различных ORM для реляционных баз данных с такими языками программирования как Python, TypeScript, JavaScript и PHP. Исходя из этого я опишу то, что мне нравится в этой библиотеке и что нет. Я буду использовать язык программирования TypeScript и MySQL в качестве базы данных. Yarn будет использоваться для инициализации проекта и управления зависимостями. Я не ожидаю каких-то глубоких знаний этих технологий и мои примеры будут по большей части объяснять сами себя.
По большей части я буду использовать официальную документацию.
Давайте создадим проект и инициализируем его.
mkdir prisma_orm_review
cd prisma_orm_review
yarn init -y
yarn add -D typescript ts-node @types/node
Добавим конфигурацию для TypeScript компилятора в tsconfig.json
файл.
touch tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
Установим Prisma ORM
yarn add prisma
Инициализация Prisma ORM для базы данных MySQL.
npx prisma init --datasource-provider mysql
Модель данных
Prisma ORM использует несколько нестандартный подход к модели данных. Для того чтобы определить модели, необходимо использовать специальный файл и специфичный для этой библиотеки синтаксис. Он несколько менее гибок, чем моделирование при помощи аннотаций в *.ts
файлах, но при этом легче читается. Большая часть типов данных, поддерживаемая в различных базах данных имеет поддержку в Prisma ORM, но в то же время есть ряд исключений, которые могут повлиять на выбор библиотеки для работы с данными.
model Account {
id Int @id @default(autoincrement())
parentAccountId Int?
parentAccount Account? @relation("Parent", fields: [parentAccountId], references: [id])
childAccounts Account[] @relation("Parent")
email String @unique
name String?
meta Json
isActive Boolean @default(false)
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
account Account @relation(fields: [accountId], references: [id])
accountId Int
}
На основании схемы Prisma ORM автоматически сгенерирует код для модели данных и миграции, для изменения базы данных.
npx prisma migrate dev --name init
Простые запросы к базе данных
Следующий скрипт позволяет сразу создать новый аккаунт и пост. Prisma ORM клиент позволяет создавать одновременно несколько связанных сущностей в одной транзакции, что облегчает работу со связанными сущностями.
Я настоятельно рекомендую включать логирование запросов к базе данных при разработке приложения. Это поможет отслеживать реальные запросы и понять какие запросы выполняются быстрее или медленнее. Конкретный пример я приведу ниже
script.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient({
log: ["query", "info", "warn", "error"],
});
async function main() {
// create an account
const accountWithPost = await prisma.account.create({
data: {
email: "hello@exmaple.com",
name: "John Doe",
meta: {
firstName: "John",
lastName: "Doe",
age: 30,
},
isActive: true,
posts: {
create: {
title: "Post #1",
content: "Hello world",
published: true,
},
},
},
});
console.log(accountWithPost);
// select active accounts
const activeAccounts = await prisma.account.findMany({
where: {
isActive: true,
},
});
console.group(activeAccounts);
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
BEGIN;
INSERT
INTO
cta.Account (id, email, name, meta, isActive, createdAt)
VALUES
(?, ?, ?, ?, ?, ?);
INSERT
INTO
cta.Post (id, title, content, published, accountId)
VALUES
(?, ?, ?, ?, ?);
SELECT
cta.Account.id,
cta.Account.parentAccountId,
cta.Account.email,
cta.Account.name,
cta.Account.meta,
cta.Account.isActive,
cta.Account.createdAt
FROM
cta.Account
WHERE
cta.Account.id = ?
LIMIT ? OFFSET ?;
COMMIT;
Следующий запрос достанет из базы данных все активные аккаунты. В нашей безе есть всего лишь один аккаунт, поэтому такой запрос безопасен. В случае большой базы данных необходимо разбивать результат на страницы для избежания переполнения памяти.
SELECT
cta.Account.id,
cta.Account.parentAccountId,
cta.Account.email,
cta.Account.name,
cta.Account.meta,
cta.Account.isActive,
cta.Account.createdAt
FROM
cta.Account
WHERE
cta.Account.isActive = ?
Использование «сырых» запросов
Теперь давайте достанем из базы первые 10 уникальных имен аккаунтов.
async function main() {
const result = await prisma.account.findMany({
select: {
name: true,
},
where: {},
distinct: ["name"],
take: 10,
skip: 0,
});
console.log(result);
}
Код выглядит довольно просто. Но нас ожидает сюрприз в логах. SQL запрос к базе данных выглядит следующим образом:
SELECT
cta.Account.id,
cta.Account.name
FROM
cta.Account
WHERE
1 = 1
ORDER BY
cta.Account.id ASC
Как вы видите, в этом запросе не используются лимиты, прописанные в запросе. Это потому, что Prisma не использует `DISTINCT`
в запросах и фильтрует данные в памяти. Это крайне сомнительное решение. Оно будет работать когда в базе есть несколько тысяч записей, но перестанет если там будет несколько миллионов. Да и вообще крайне странно доставать из базы миллион записей для того, чтобы отфильтровать первые 10 уникальных имен.
В этом случае необходимо уже спускаться на уровень «сырых» запросов к базе данных.
async function main() {
const limit = 10;
const result = await prisma.$queryRaw>(
Prisma.sql`SELECT DISTINCT
(name)
FROM
Account
LIMIT ${limit}`
);
console.log(result);
}
Полученный SQL запрос:
select distinct(name) from Account limit ?
На первый взгляд код из этого примера выглядит как классический пример из книги «Как сделать SQL инъекцию». Но на самом деле это не так. Под капотом Prisma делает немного магии, которая позволяет избежать уязвимостей.
Результат следующего кода это не просто строка с подставленными в нее значениями переменных. Prisma генерирует объект с запросом и параметрами этого запроса.
console.log(Prisma.sql`select distinct(name) from Account limit ${limit`);
{
text: 'select distinct(name) from Account limit $1',
sql: 'select distinct(name) from Account limit ?',
values: [ 10 ]
}
Все переменные заменены на плейсхолдеры и поэтому такой запрос безопасен. В то же время такой подход может вызвать проблемы при составлении более сложных запросов. Пример я приведу в следующем разделе.
«Сырые» небезопасные запросы
Представьте себе ситуацию, в которой вам нужно фильтровать данные по полям из JSON объекта `meta`
. При этом, то, по каким полям производить фильтрацию тоже определяется на уровне кода.
async function main() {
const limit = 10;
const jsonFieldName = "firstName";
const jsonFieldValue = "John";
const result = await prisma.$queryRaw>(
Prisma.sql`SELECT DISTINCT
(name)
FROM
Account
WHERE
meta ->> "$.${jsonFieldName}" = ${jsonFieldValue}
LIMIT ${limit};
`
);
}
Полученный запрос работать не будет. В этом случае эта магия, которая работает под капотом `Prisma.sql`
не позволит нам просто взять и собрать запрос из нескольких строк.
SELECT DISTINCT
(name)
FROM
Account
WHERE
meta ->> "$.?" = ?
LIMIT ?;
Поэтому пришло время для использования `$queryRawUnsafe`
. Мы все еще будем использовать параметры в запросе для значений JSON полей и лимитов
async function main() {
const limit = 10;
const jsonFieldName = "firstName";
const jsonFieldValue = "John";
const result = await prisma.$queryRawUnsafe>(
`SELECT DISTINCT
(name)
FROM
Account
WHERE
meta ->> "$.${jsonFieldName}" = ?
LIMIT ?;
`,
jsonFieldValue,
limit
);
console.log(result);
}
Полученный SQL:
SELECT DISTINCT
(name)
FROM
Account
WHERE
meta ->> "$.firstName" = ?
LIMIT ?;
Этот запрос выглядит довольно неплохо, но все же я крайне не рекомендую использовать его в реальной работе. Он никогда не будет использовать индексы в вашей базе и поэтому всегда будет выполняться медленно.
Выводы
В целом Prisma ORM — это неплохой выбор для проекта. Можно подчеркнуть, что не хватает возможности на уровне построителя запросов сделать `distinct`
, но при этом достаточно просто двинуться в сторону «сырых» запросов к базе данных.