قسمت دهم - اتصال Next.js به PostgreSQL با Prisma
وقتی قرار است Next.js را به دیتابیس وصل کنیم، فقط به یک اتصال خام نیاز نداریم؛ به ابزاری نیاز داریم که کار با داده را قابلفهم، type-safe و قابل نگهداری کند.
وقتی قرار است Next.js را به دیتابیس وصل کنیم، فقط به یک اتصال خام نیاز نداریم؛
به ابزاری نیاز داریم که کار با داده را قابلفهم، type-safe و قابل نگهداری کند.
وقتی از یک پرتفولیوی ساده عبور میکنیم و وارد ساخت یک پروژه واقعی میشویم، خیلی زود به یک نیاز مشترک میرسیم:
باید دادهها جایی خارج از کد ذخیره شوند.
تا وقتی پروژهها، پستها، اطلاعات About یا محتوای CMS را با آرایههای دستی داخل فایلها نگه میداریم، توسعه سریع است؛
اما این مدل فقط برای شروع مناسب است.
در یک پروژه واقعی، باید بتوانیم داده را ذخیره کنیم، ویرایش کنیم، منتشر کنیم و بعد در صفحههای مختلف نمایش دهیم.
در این مرحله، ترکیب PostgreSQL و Prisma یکی از انتخابهای استاندارد و عملی برای پروژههای Next.js است.
در این نوشته، قدمبهقدم این مسیر را میسازیم:
- راهاندازی
Prisma - اتصال به
PostgreSQL - طراحی مدلهای داده
- اجرای
migration - ساخت
Prisma Client - و در نهایت، ساخت یک
data layerساده برای استفاده در پروژه
چرا Prisma؟
وقتی قرار است Next.js را به دیتابیس وصل کنیم، فقط به یک اتصال خام نیاز نداریم؛
به ابزاری نیاز داریم که کار با داده را شفاف، type-safe و قابل نگهداری کند.
Prisma دقیقاً همینجا ارزشش را نشان میدهد.
چند دلیل مهم برای انتخاب Prisma:
- تعریف شفاف مدلها: ساختار داده را در
schema.prismaمشخص میکنیم - Type safety: کوئریها و دادههای برگشتی با
TypeScriptهماهنگ هستند - Migration workflow: تغییرات مدل را میتوانیم بهصورت کنترلشده به دیتابیس اعمال کنیم
- Prisma Client: برای query گرفتن نیازی به نوشتن
SQLخام در همهجا نداریم - خوانایی بهتر کد: منطق کار با داده مرتبتر از queryهای پراکنده میشود
برای پروژهای مثل پرتفولیو، بلاگ، داشبورد و CMS، این مزیتها مهماند؛
چون قرار نیست فقط یک جدول ساده داشته باشیم. معمولاً بهمرور با relationها، slugها، وضعیت draft/published و پنل مدیریت محتوا روبهرو میشویم.
راهاندازی Prisma
در این پروژه از npm استفاده میکنیم.
برای شروع، پکیجهای لازم را نصب میکنیم:
npm install prisma @prisma/client
npm install @prisma/adapter-pg pg
npm install -D tsx
در این سری آموزشی، چون از PostgreSQL استفاده میکنیم و میخواهیم با الگوی بهروز Prisma جلو برویم، از @prisma/adapter-pg هم استفاده میکنیم.
نقش هرکدام:
prisma: ابزارCLIبرایinit،migrateوgenerate@prisma/client: کلاینتی که در کد پروژه با آن query میگیریمpg: درایورPostgreSQL@prisma/adapter-pg: آداپتر برای setup جدیدPrismaباPostgreSQLtsx: برای اجرای فایلهایTypeScriptدر بعضی workflowها
بعد از نصب، Prisma را initialize میکنیم:
npx prisma init
این دستور معمولاً فایلها و ساختار اولیه زیر را میسازد:
prisma/schema.prisma
.env
در بعضی setupها ممکن است فایلهای دیگری هم ببینی، اما برای شروع، همین دو فایل مهمترین بخش هستند.
اتصال به PostgreSQL
بعد از initialize شدن Prisma، مهمترین بخش اتصال، تنظیم DATABASE_URL است.
داخل فایل .env معمولاً چیزی شبیه این قرار میگیرد:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"
فرقی نمیکند دیتابیس روی localhost باشد یا روی سرویسی مثل:
NeonSupabaseRailwayRender- یا هر سرویس دیگری که
PostgreSQLارائه میدهد
در هر صورت، Prisma به یک connection string معتبر نیاز دارد.
مثال:
DATABASE_URL="postgresql://postgres:123456@localhost:5432/portfolio_db?schema=public"
در این مرحله مسئولیت ما روشن است:
- یک دیتابیس
PostgreSQLبسازیم connection stringرا بگیریم- آن را داخل
.envدرDATABASE_URLقرار بدهیم
نکته مهم
فایل .env نباید وارد مخزن عمومی شود، مگر اینکه ساختار پروژهات از قبل برای این موضوع کنترل شده باشد.
اطلاعات اتصال به دیتابیس جزو دادههای حساس پروژه هستند.
طراحی مدلهای داده
بعد از اتصال، نوبت مهمترین بخش معماری داده است: مدلسازی.
در Prisma، مدلها داخل prisma/schema.prisma تعریف میشوند.
اینجا مشخص میکنیم چه entityهایی در پروژه داریم و هرکدام چه فیلدهایی دارند.
برای یک پروژه پرتفولیو، خیلی محتمل است که با مدلهایی مثل این سروکار داشته باشیم:
ProjectPostUser- و شاید بعداً
Tag،CategoryیاContactMessage
اگر بخواهیم از یک مدل ساده برای پروژهها شروع کنیم، میتوانیم چیزی شبیه این داشته باشیم:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Project {
id String @id @default(cuid())
title String
slug String @unique
description String
content String?
category String?
technologies String[]
demoUrl String?
githubUrl String?
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
اگر بخواهیم برای بلاگ هم آماده باشیم، میتوانیم بعداً مدلی شبیه این اضافه کنیم:
model Post {
id String @id @default(cuid())
title String
slug String @unique
excerpt String?
content String
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
در طراحی مدل به چه چیزهایی فکر کنیم؟
برای پروژه واقعی، فقط داشتن چند فیلد کافی نیست. بهتر است از همان ابتدا به این سؤالها فکر کنیم:
- آیا این محتوا
publishedدارد یاdraftهم خواهیم داشت؟ - آیا برای routeها به
slugنیاز داریم؟ - آیا فیلدی باید
uniqueباشد؟ - آیا بعداً
searchیاfilteringخواهیم داشت؟ - آیا بین مدلها
relationلازم است؟
مدلسازی خوب یعنی ساختاری بنویسیم که هم نیاز فعلی را پوشش دهد، هم با رشد پروژه نشکند.
migration
بعد از اینکه مدلها را در schema.prisma تعریف کردیم، هنوز هیچ جدولی در دیتابیس ساخته نشده است.
برای اعمال این تغییرات باید migration اجرا کنیم.
دستور معمول:
npx prisma migrate dev --name init
این دستور چند کار مهم انجام میدهد:
schemaفعلی را بررسی میکند- فایل
migrationمیسازد - تغییرات را روی دیتابیس اعمال میکند
- در بسیاری از workflowها
Prisma Clientرا هم regenerate میکند
اگر بعداً مدلها را تغییر بدهیم، دوباره migration جدید میسازیم. مثلاً:
npx prisma migrate dev --name add-post-model
نکته مهم این است که migration فقط یک دستور برای ساخت جدول نیست؛
در واقع تاریخچه تغییرات ساختار دیتابیس ما است.
برای همین، migrationها بخش مهمی از نگهداری پروژه هستند.
اگر بخواهیم فقط Prisma Client را دوباره تولید کنیم، از این دستور استفاده میکنیم:
npx prisma generate
Prisma Client
بعد از اینکه schema آماده شد و migration اجرا شد، وقت استفاده از Prisma Client است.
این همان ابزاری است که در کد Next.js با آن query میگیریم.
setup ما میتواند شبیه این باشد:
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import { Pool } from 'pg'
const globalForPrisma = globalThis as unknown as {
db: PrismaClient | undefined
pgPool: Pool | undefined
}
const connectionString = process.env.DATABASE_URL
if (!connectionString) {
throw new Error('DATABASE_URL is not set.')
}
const pool =
globalForPrisma.pgPool ??
new Pool({
connectionString,
})
const adapter = new PrismaPg(pool)
export const db =
globalForPrisma.db ??
new PrismaClient({
adapter,
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.pgPool = pool
globalForPrisma.db = db
}
این فایل را میتوانیم در مسیر زیر قرار بدهی:
lib/db.ts
چرا این الگو مهم است؟
چند دلیل دارد:
- یک instance مرکزی برای کار با دیتابیس داریم
- از setup جدید
Prismaبا adapter استفاده میکنیم - در development جلوی ساخت instanceهای غیرضروری گرفته میشود
- در پستهای بعدی، همین
dbرا مستقیم داخلServer Actionsاستفاده خواهیم کرد
ساخت لایه دسترسی به داده
تا اینجا به دیتابیس وصل شدیم و Prisma Client هم آماده است.
اما یک نکته مهم معماری باقی مانده:
بهتر است queryها را مستقیم در همه pageها و componentها نریزیم.
اینجاست که data layer وارد میشود.
یعنی بهجای این:
const projects = await db.project.findMany()
در هرجایی از پروژه، توابع مشخصی بسازیم که مسئول خواندن داده باشند.
مثلاً:
// lib/data/projects.ts
import { db } from '@/lib/db'
export async function getProjects() {
return db.project.findMany({
where: {
published: true,
},
orderBy: {
createdAt: 'desc',
},
})
}
export async function getProjectBySlug(slug: string) {
return db.project.findUnique({
where: {
slug,
},
})
}
این الگو چند مزیت جدی دارد:
- منطق query از UI جدا میشود
reuseراحتتر میشود- تغییرات بعدی سادهتر میشوند
pageها وsectionها تمیزتر میمانند- تستپذیری بهتر میشود
برای مثال، در app/projects/page.tsx میتوانیم فقط این را داشته باشیم:
import { getProjects } from '@/lib/data/projects'
export default async function ProjectsPage() {
const projects = await getProjects()
return (
<div>
{projects.map((project) => (
<div key={project.id}>{project.title}</div>
))}
</div>
)
}
در این نقطه، دیگر page وظیفه ساخت query ندارد؛ فقط داده را میگیرد و نمایش میدهد.
در این مرحله تمرکز ما روی ساخت زیرساخت است:
- دیتابیس
schemamigrationPrisma Clientdata layer
یعنی فعلاً داریم خواندن و سازماندهی داده را آماده میکنیم.
بخش ذخیرهسازی، ویرایش و حذف داده را در پستهای بعدی با استفاده از Server Actions انجام میدهیم؛
چون آنجا باید علاوه بر Prisma، به validation، auth، revalidatePath و redirect هم توجه کنیم.
جمعبندی
برای اینکه یک پروژه Next.js از مرحله mock data عبور کند و وارد فاز واقعی شود، باید یک data layer قابل اتکا داشته باشد.
ترکیب PostgreSQL و Prisma یکی از بهترین مسیرها برای رسیدن به این هدف است.
در این مسیر:
PostgreSQLدادههای پروژه را نگه میداردPrismaمدلسازی وmigrationرا مدیریت میکندPrisma Clientراهی type-safe برای query گرفتن میدهدdata layerکمک میکند منطق دسترسی به داده از UI جدا بماند
نتیجه این است که صفحههایی مثل projects، blog، dashboard و CMS روی یک پایه واقعی و قابل نگهداری ساخته میشوند، نه روی آرایههای موقتی داخل فایلها.