Пошук уроків, статей та іншого контенту
Опишемо зв’язки між моделями та навчимося читати, створювати й оновлювати пов’язані записи.
Relation — це зв’язок між двома моделями бази даних. Наприклад:
один користувач має багато публікацій;
одна публікація належить одному користувачу;
користувач має один профіль;
публікація може мати багато категорій, а категорія — багато публікацій.
У Prisma relation складається з:
relation-полів — властивостей-масивів або об’єктів, через які Prisma працює зі зв’язком;
foreign key — поля, яке фактично зберігає ідентифікатор пов’язаної записи;
атрибута @relation, який описує, як саме моделі пов’язані.
Один користувач має один профіль, і один профіль належить одному користувачу.
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}Поле userId є зовнішнім ключем. Атрибут @unique гарантує, що один користувач не матиме кількох профілів.
Поле profile у User має тип Profile?, тому що профіль може ще не існувати.
Один користувач має багато публікацій, але кожна публікація належить одному користувачу.
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
authorId Int
author User @relation(fields: [authorId], references: [id])
}У цьому випадку:
User.posts — колекція публікацій;
Post.author — один автор;
Post.authorId — зовнішній ключ.
Оскільки authorId не має ?, кожна публікація повинна мати автора.
Публікація може належати до кількох категорій, а категорія може використовуватися в багатьох публікаціях.
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}Prisma автоматично створить проміжну таблицю для такого зв’язку. У моделях достатньо вказати масиви з обох боків relation.
Після додавання або зміни моделей потрібно синхронізувати структуру бази даних:
npx prisma migrate dev --name add-relationsПісля цього Prisma Client буде згенерований або оновлений автоматично.
За замовчуванням Prisma повертає лише поля основної моделі. Пов’язані записи потрібно запитати явно за допомогою include або select.
includeinclude додає relation до результату:
const user = await prisma.user.findUnique({
where: {
id: 1,
},
include: {
profile: true,
posts: true,
},
});Результат міститиме користувача, його профіль і всі публікації.
Можна завантажувати relation на кілька рівнів:
const user = await prisma.user.findUnique({
where: {
id: 1,
},
include: {
posts: {
include: {
categories: true,
},
},
},
});Тут разом із користувачем будуть отримані:
його публікації;
категорії кожної публікації.
До relation можна застосовувати фільтри, сортування та пагінацію:
const user = await prisma.user.findUnique({
where: {
id: 1,
},
include: {
posts: {
where: {
title: {
contains: 'Prisma',
},
},
orderBy: {
id: 'desc',
},
take: 10,
},
},
});У результат потраплять лише публікації, у заголовку яких є Prisma.
selectselect дає змогу вибрати конкретні поля:
const user = await prisma.user.findUnique({
where: {
id: 1,
},
select: {
id: true,
email: true,
posts: {
select: {
id: true,
title: true,
},
},
},
});select корисний, коли не потрібно повертати всі поля моделі, наприклад content або службові поля.
На одному рівні запиту використовують або include, або select. Змішувати їх на одному рівні не можна.
Prisma підтримує вкладені операції. Це дає змогу створити основний запис і пов’язані записи одним запитом.
const user = await prisma.user.create({
data: {
email: 'anna@example.com',
profile: {
create: {
bio: 'Backend developer',
},
},
},
include: {
profile: true,
},
});Prisma самостійно створить User, Profile і встановить userId.
Для зв’язування з наявним записом використовується connect:
const post = await prisma.post.create({
data: {
title: 'Relations у Prisma',
content: 'Матеріал про зв’язки між моделями',
author: {
connect: {
id: 1,
},
},
},
include: {
author: true,
},
});У цьому випадку Prisma не створює нового користувача, а під’єднує публікацію до користувача з id = 1.
Можна напряму передати foreign key:
const post = await prisma.post.create({
data: {
title: 'Інша публікація',
authorId: 1,
},
});connect є більш декларативним і зручним, коли операція містить кілька вкладених relation.
Коли нова публікація одразу має кілька категорій, можна використати вкладений create:
const post = await prisma.post.create({
data: {
title: 'NestJS і Prisma',
author: {
connect: {
id: 1,
},
},
categories: {
create: [
{ name: 'NestJS' },
{ name: 'Prisma' },
],
},
},
include: {
categories: true,
},
});Цей приклад створює публікацію, дві категорії та зв’язки між ними.
Якщо категорії вже існують, використовуйте connect:
const post = await prisma.post.create({
data: {
title: 'Публікація з наявними категоріями',
author: {
connect: {
id: 1,
},
},
categories: {
connect: [
{ id: 2 },
{ id: 3 },
],
},
},
});connect працює лише тоді, коли записи з такими ідентифікаторами вже існують.
connectOrCreateІноді потрібно під’єднати наявну категорію або створити її, якщо вона ще не існує.
const post = await prisma.post.create({
data: {
title: 'Нова стаття',
author: {
connect: {
id: 1,
},
},
categories: {
connectOrCreate: [
{
where: {
name: 'Backend',
},
create: {
name: 'Backend',
},
},
{
where: {
name: 'TypeScript',
},
create: {
name: 'TypeScript',
},
},
],
},
},
});Для connectOrCreate поле, вказане в where, повинно бути унікальним. У нашому прикладі це Category.name @unique.
Профіль можна оновити через relation:
const user = await prisma.user.update({
where: {
id: 1,
},
data: {
profile: {
update: {
bio: 'Senior backend developer',
},
},
},
include: {
profile: true,
},
});Це змінює профіль користувача, але не створює новий профіль.
Якщо профіль може бути відсутнім, його можна створити під час оновлення користувача:
const user = await prisma.user.update({
where: {
id: 1,
},
data: {
profile: {
upsert: {
create: {
bio: 'Новий профіль',
},
update: {
bio: 'Оновлений профіль',
},
},
},
},
include: {
profile: true,
},
});upsert виконає одну з двох дій:
оновить relation, якщо запис уже існує;
створить relation, якщо запису ще немає.
const user = await prisma.user.update({
where: {
id: 1,
},
data: {
posts: {
connect: {
id: 10,
},
},
},
include: {
posts: true,
},
});Це під’єднає публікацію з ідентифікатором 10 до користувача.
Оскільки в нашій схемі Post.authorId є обов’язковим, публікація вже повинна мати автора. Під час connect Prisma змінить її автора на вказаного користувача.
const user = await prisma.user.update({
where: {
id: 1,
},
data: {
posts: {
create: {
title: 'Публікація, створена через користувача',
content: 'Текст публікації',
},
},
},
include: {
posts: true,
},
});Prisma автоматично встановить authorId для нової публікації.
Для relation типу «багато до багатьох» можна замінити весь набір пов’язаних категорій через set:
const post = await prisma.post.update({
where: {
id: 10,
},
data: {
categories: {
set: [
{ id: 2 },
{ id: 5 },
],
},
},
include: {
categories: true,
},
});Після цього публікація матиме лише категорії з ідентифікаторами 2 і 5.
set змінює зв’язки, але не видаляє самі записи категорій із таблиці.
Щоб додати категорію, використовуйте connect:
await prisma.post.update({
where: {
id: 10,
},
data: {
categories: {
connect: {
id: 7,
},
},
},
});Щоб видалити лише зв’язок із категорією, використовуйте disconnect:
await prisma.post.update({
where: {
id: 10,
},
data: {
categories: {
disconnect: {
id: 7,
},
},
},
});Категорія залишиться в базі даних, але більше не буде пов’язана з цією публікацією.
У NestJS Prisma Client зазвичай доступний через окремий PrismaService.
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class PostsService {
constructor(private readonly prisma: PrismaService) {}
async findOne(id: number) {
const post = await this.prisma.post.findUnique({
where: {
id,
},
include: {
author: {
select: {
id: true,
email: true,
},
},
categories: true,
},
});
if (!post) {
throw new NotFoundException('Публікацію не знайдено');
}
return post;
}
async create(
title: string,
content: string | undefined,
authorId: number,
categoryIds: number[],
) {
return this.prisma.post.create({
data: {
title,
content,
author: {
connect: {
id: authorId,
},
},
categories: {
connect: categoryIds.map((id) => ({ id })),
},
},
include: {
author: true,
categories: true,
},
});
}
async updateCategories(postId: number, categoryIds: number[]) {
return this.prisma.post.update({
where: {
id: postId,
},
data: {
categories: {
set: categoryIds.map((id) => ({ id })),
},
},
include: {
categories: true,
},
});
}
}У методі create масив числових ідентифікаторів перетворюється у формат, який очікує Prisma:
categoryIds.map((id) => ({ id }))Наприклад, масив:
[2, 5, 8]перетворюється на:
[
{ id: 2 },
{ id: 5 },
{ id: 8 },
]Цей формат використовується для connect і set.
Якщо передати в connect ідентифікатор неіснуючого запису, Prisma поверне помилку. У прикладному коді це часто потрібно перетворити на зрозумілу HTTP-помилку.
import {
BadRequestException,
Injectable,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class PostsService {
constructor(private readonly prisma: PrismaService) {}
async createWithAuthor(title: string, authorId: number) {
const author = await this.prisma.user.findUnique({
where: {
id: authorId,
},
});
if (!author) {
throw new BadRequestException('Вказаного автора не існує');
}
return this.prisma.post.create({
data: {
title,
authorId,
},
});
}
}Попередня перевірка дає змогу повернути клієнту контрольовану помилку, а не необроблену помилку Prisma.
Вкладені операції Prisma виконуються як одна логічна операція. Наприклад, під час створення користувача з профілем Prisma створює обидва записи та встановлює зв’язок між ними.
Однак потрібно пам’ятати:
connect під’єднує лише наявні записи;
create створює новий пов’язаний запис;
connectOrCreate під’єднує або створює запис;
set замінює весь набір зв’язків;
disconnect видаляє зв’язок, але не сам запис;
delete видаляє пов’язаний запис;
include повертає relation у результаті;
select обмежує поля, які повертаються.
Для relation Prisma потрібні поля з обох боків:
model User {
posts Post[]
}
model Post {
author User
}Якщо вказати relation лише в одній моделі, Prisma не зможе коректно побудувати схему.
@unique у зв’язку один до одногоУ зв’язку один до одного foreign key повинен бути унікальним:
userId Int @uniqueБез @unique один користувач зможе мати кілька профілів, і це вже буде зв’язок «один до багатьох».
include та select разомНекоректний запит:
await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
},
select: {
id: true,
},
});На одному рівні потрібно вибрати один підхід: або include, або select.
connectДля під’єднання наявного запису потрібна вкладена операція:
await prisma.post.create({
data: {
title: 'Заголовок',
author: {
connect: {
id: 1,
},
},
},
});У разі прямого передавання foreign key використовуйте:
await prisma.post.create({
data: {
title: 'Заголовок',
authorId: 1,
},
});disconnect для обов’язкового relationЯкщо модель має обов’язковий foreign key:
authorId Intпублікацію не можна від’єднати від автора, не призначивши іншого автора.
Щоб relation міг бути відсутнім, foreign key і relation повинні бути optional:
model Post {
id Int @id @default(autoincrement())
title String
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}disconnect видаляє лише зв’язок.
categories: {
disconnect: {
id: 7,
},
}Категорія залишиться в базі даних.
delete видаляє сам пов’язаний запис:
profile: {
delete: true,
}Це різні операції, тому перед використанням delete потрібно переконатися, що запис справді більше не потрібен.
Relation у Prisma описує зв’язок між моделями бази даних.
Основні типи зв’язків: один до одного, один до багатьох і багато до багатьох.
Foreign key зберігається в моделі, яка посилається на іншу модель.
include і select використовуються для читання пов’язаних записів.
create створює пов’язані записи вкладено.
connect під’єднує наявні записи.
connectOrCreate під’єднує запис або створює його за відсутності.
set повністю замінює набір зв’язків.
connect і disconnect змінюють окремі зв’язки.
disconnect не видаляє запис, а delete видаляє його.
У NestJS relation-операції виконуються через PrismaService у сервісах застосунку.