Next.js: دمج قواعد البيانات: Prisma
آخر تحديث: 2026-08-26
Prisma هو أكثر ORM شيوعًا في نظام Node.js البيئي — يتيح لك كتابة نماذج البيانات بلغة TypeScript ويولد تلقائيًا عملاء قاعدة بيانات آمنين النوع.
1. ما ستتعلمه
- تثبيت وتهيئة وإعداد هيكل المشروع لـ Prisma ORM
- نمذجة بيانات Schema (علاقات User / Post / Comment)
- ترحيل قاعدة البيانات (
prisma migrate dev) وتصور Prisma Studio - قراءة البيانات في Server Component؛ كتابة البيانات في Server Action
- عمليات CRUD في Route Handlers
- إدارة تجمع اتصالات Prisma والتوافق مع Next.js
2. قصة حقيقية لمهندس Full-Stack
(1) نقطة الألم: استخدام SQL وحده يسبب انخفاضًا حادًا في كفاءة التطوير
Bob هو القائد التقني لفريق TaskFlow. يستخدم الفريق SQL الأصلي للعمل مع PostgreSQL:
"عليك كتابة 20 سطرًا من كود SQL القالب لكل نقطة نهاية API. مع
JOIN، من السهل تفويت الحقول في الاستعلامات، وعليك صيانة نصوص migration يدويًا عند تغيير هيكل الجدول. الأمر الأكثر إحباطًا هو أن أنواع TypeScript غير متزامنة مع حقول قاعدة البيانات — لا تدرك أنك كتبت أسماء الأعمدة بشكل خاطئ إلا في وقت التشغيل."
| المشكلة | الوقت المستغرق أسبوعيًا | التأثير |
|---|---|---|
| كتابة قوالب SQL يدويًا | 8 ساعات | عمل متكرر |
| تصحيح عدم تطابق الأنواع | 4 ساعات | خطأ وقت التشغيل |
| نص migration يدوي | 3 ساعات | عرضة للسهو |
| التوثيق غير محدث | ساعتان | يحتاج الوافدون الجدد وقتًا طويلاً للتأقلم |
(2) حل Prisma ORM
تعريف نموذج باستخدام Prisma Schema ← توليد أنواع TypeScript تلقائيًا ← CRUD آمن النوع.
// prisma/schema.prisma — نموذج البيانات كتوثيق
model User {
id String @id @default(cuid())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
(3) النتائج
| البعد | SQL المكتوب يدويًا | Prisma |
|---|---|---|
| حجم الكود لكل عملية CRUD في API | 25 سطرًا | 3 أسطر |
| أمان الأنواع | ❌ تعريف يدوي | ✅ توليد تلقائي |
| إدارة الترحيل | ملفات SQL يدوية | prisma migrate dev |
| تجربة التطوير | التنقل بين IDE/عملاء DB | مدمج في Prisma Studio |
| مزامنة التوثيق | إضافات خارجية | Schema كتوثيق |
3. تثبيت Prisma وتهيئة المشروع
(1) عملية التثبيت
# 1. تثبيت Prisma CLI والعميل
npm install prisma @prisma/client --save-dev
# أو دفعة واحدة
npx prisma init --datasource-provider postgresql
graph LR
A[prisma init] --> B[إنشاء prisma/schema.prisma]
A --> C[إنشاء .env مع DATABASE_URL]
B --> D[تعريف نموذج البيانات]
D --> E[prisma migrate dev]
E --> F[توليد Prisma Client]
F --> G[استيراد واستخدام]
style A fill:#cce5ff
style D fill:#d4edda
style F fill:#fff3cd
| الملف المُنشأ | الغرض |
|---|---|
prisma/schema.prisma |
تعريف نموذج البيانات |
.env |
سلسلة اتصال قاعدة البيانات (DATABASE_URL) |
(2) تهيئة Singleton للعميل
// lib/prisma.ts — Singleton عام (يمنع إنشاء اتصالات متعددة أثناء إعادة التحميل الساخن)
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
▶ مثال: التحقق من اتصال قاعدة البيانات
المخرجات:
تم تنفيذ عملية قاعدة البيانات بنجاح.
// app/api/health/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
try {
await prisma.$connect()
return NextResponse.json({ status: 'ok', db: 'connected' })
} catch (e) {
return NextResponse.json({ status: 'error', message: (e as Error).message }, { status: 500 })
}
}
{ "status": "ok", "db": "connected" }
المخرجات:
{ "status": "ok", "db": "connected" } ← JSON بمفتاحين: status ("ok") و db ("connected")
4. نمذجة بيانات Schema
(1) أنواع علاقات النماذج
graph TB
User -->|واحد إلى متعدد| Post
User -->|واحد إلى متعدد| Comment
Post -->|واحد إلى متعدد| Comment
Post -->|متعدد إلى متعدد| Tag
subgraph User
U1[id String @id @default(cuid())]
U2[name String]
U3[email String @unique]
end
subgraph Post
P1[id String @id @default(cuid())]
P2[title String]
P3[content String?]
end
subgraph Comment
C1[id String @id @default(cuid())]
C2[body String]
end
subgraph Tag
T1[id String @id @default(cuid())]
T2[name String @unique]
end
style User fill:#d4edda
style Post fill:#cce5ff
style Comment fill:#f8d7da
style Tag fill:#fff3cd
| نوع العلاقة | صيغة Prisma | التنفيذ في قاعدة البيانات |
|---|---|---|
| واحد إلى واحد | User Profile |
مفتاح خارجي + فريد |
| واحد إلى متعدد | User Post[] |
مفتاح خارجي |
| متعدد إلى متعدد | Post Tag[] (ضمني) |
جدول وسيط _PostToTag |
| مرجع ذاتي | Category parentCategory |
مفتاح خارجي ذاتي المرجع |
(2) مخطط TaskFlow الكامل
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
EDITOR
VIEWER
}
model User {
id String @id @default(cuid())
name String
email String @unique
role Role @default(VIEWER)
password String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String
tags Tag[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
model Comment {
id String @id @default(cuid())
body String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
author String
createdAt DateTime @default(now())
@@index([postId])
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
@@index([name])
}
▶ مثال: عمليات تصور Prisma Studio
# تشغيل Prisma Studio (واجهة متصفح رسومية لعرض/تحرير البيانات)
npx prisma studio
1. شغل `npx prisma studio`
2. افتح في المتصفح http://localhost:5555
3. اختر من اليسار جداول User / Post / Comment / Tag
4. انقر "Add Record" لإضافة بيانات اختبارية
5. انقر "Save Changes" للحفظ
5. ترحيل قاعدة البيانات (Migration)
(1) سير عمل الترحيل
| الخطوة | الأمر | الوظيفة |
|---|---|---|
| تعديل Schema | تحرير schema.prisma |
إضافة أو إزالة نماذج/حقول |
| إنشاء ترحيل | npx prisma migrate dev --name add_user_role |
توليد ملف SQL للترحيل |
| تطبيق الترحيل | تلقائي | تحديث هيكل قاعدة البيانات |
| إعادة تعيين قاعدة البيانات | npx prisma migrate reset |
مسح البيانات + إعادة الترحيل |
| توليد العميل | npx prisma generate |
تحديث أنواع TypeScript |
(2) هيكل ملفات الترحيل
prisma/migrations/
├── 20260706000001_init/
│ └── migration.sql # إنشاء الجداول الأولية
├── 20260706000002_add_user_role/
│ └── migration.sql # ALTER TABLE إضافة عمود role
└── migration_lock.toml # قفل مزود قاعدة البيانات
▶ مثال: إضافة ترحيل الدور
npx prisma migrate dev --name add_role_enum
SQL المُنشأ:
-- prisma/migrations/20260706000002_add_role_enum/migration.sql
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'EDITOR', 'VIEWER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'VIEWER';
المخرجات:
تم تنفيذ العبارة (العبارات) بنجاح.
prisma migrate dev يكتشف تلقائيًا تغييرات schema ويولد عبارات SQL المقابلة. لا حاجة لكتابة عبارات ALTER TABLE يدويًا.
6. عمليات CRUD العملية
(1) Server Component يقرأ البيانات
// app/posts/page.tsx — RSC يستعلم قاعدة البيانات مباشرة
import { prisma } from '@/lib/prisma'
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: { select: { name: true } }, tags: true },
orderBy: { createdAt: 'desc' },
take: 20
})
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>المؤلف: {post.author.name}</p>
<p>{post.tags.map((t) => t.name).join('، ')}</p>
</article>
))}
</div>
)
}
(2) Server Action: كتابة البيانات
// app/actions/posts.ts — Server Action للـ CRUD
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(data: { title: string; content?: string; authorId: string }) {
const post = await prisma.post.create({ data })
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
export async function deletePost(id: string) {
await prisma.post.delete({ where: { id } })
revalidatePath('/posts')
}
▶ مثال: إنشاء مقال باستخدام نموذج و Server Action
المخرجات:
ينفذ Server Action ويستدعي revalidatePath() لتحديث ذاكرة التخزين المؤقت للصفحة.
// app/posts/new/page.tsx
import { createPost } from '@/app/actions/posts'
import { auth } from '@/auth'
export default async function NewPostPage() {
const session = await auth()
if (!session?.user) return <p>يرجى تسجيل الدخول أولاً</p>
return (
<form action={createPost}>
<input name="title" placeholder="عنوان المقال" required />
<textarea name="content" placeholder="محتوى المقال" rows={10} />
<input type="hidden" name="authorId" value={session.user.id} />
<button type="submit">نشر</button>
</form>
)
}
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
النص المرئي: نشر
// تصحيح Server Action: استقبال FormData
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const authorId = formData.get('authorId') as string
if (!title || !authorId) throw new Error('الحقول المطلوبة مفقودة')
const post = await prisma.post.create({
data: { title, content, authorId }
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
(3) Route Handler: CRUD كامل
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await prisma.post.findMany({
include: { author: true, comments: true },
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(posts)
}
export async function POST(req: Request) {
const body = await req.json()
const post = await prisma.post.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
import { prisma } from '@/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: { comments: true, tags: true }
})
if (!post) return NextResponse.json({ error: 'غير موجود' }, { status: 404 })
return NextResponse.json(post)
}
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const body = await req.json()
const post = await prisma.post.update({ where: { id }, data: body })
return NextResponse.json(post)
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
await prisma.post.delete({ where: { id } })
return NextResponse.json({ deleted: true })
}
7. إدارة تجمع الاتصالات
(1) تكوين تجمع الاتصالات
| خيار التكوين | القيمة الافتراضية | توصية الإنتاج | الوصف |
|---|---|---|---|
connection_limit |
10 | 20–50 | الحد الأقصى للاتصالات المتزامنة |
pool_timeout |
10 ثوانٍ | 30 ثانية | مهلة الاتصال |
idle_timeout |
10 ثوانٍ | 30 ثانية | مدة الاحتفاظ بالاتصالات الخاملة |
(2) تكوين معلمات تجمع الاتصالات
// lib/prisma.ts — تجمع اتصالات بدرجة إنتاجية
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL + '?connection_limit=20&pool_timeout=30&idle_timeout=30'
}
}
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
?pgbouncer=true).
▶ مثال: تكوين تجمع اتصالات Supabase
# .env — تجمع اتصالات Supabase
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
// تحسين Serverless: إعادة استخدام النسخة لكل طلب
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
export function getPrisma() {
if (!prisma) {
prisma = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } }
})
}
return prisma
}
المخرجات:
تم تنفيذ عملية قاعدة البيانات بنجاح.
8. مثال كامل: تنفيذ شامل لنظام مدونة CRUD
// app/posts/[id]/page.tsx — تفاصيل المقال + ميزة التعليقات
import { prisma } from '@/lib/prisma'
import { notFound } from 'next/navigation'
import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
async function addComment(formData: FormData) {
'use server'
const session = await auth()
if (!session?.user) throw new Error('يرجى تسجيل الدخول أولاً')
const body = formData.get('body') as string
const postId = formData.get('postId') as string
if (!body || !postId) throw new Error('حقل مفقود')
await prisma.comment.create({
data: { body, postId, author: session.user.name! }
})
revalidatePath(`/posts/${postId}`)
}
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await prisma.post.findUnique({
where: { id },
include: {
author: { select: { name: true } },
tags: true,
comments: { orderBy: { createdAt: 'desc' } }
}
})
if (!post) notFound()
return (
<div>
<h1>{post.title}</h1>
<p>المؤلف: {post.author.name}</p>
<div>{post.content}</div>
<p>الوسوم: {post.tags.map((t) => t.name).join('، ')}</p>
<hr />
<h2>التعليقات ({post.comments.length})</h2>
{post.comments.map((c) => (
<div key={c.id}>
<strong>{c.author}</strong>: {c.body}
</div>
))}
<form action={addComment}>
<input type="hidden" name="postId" value={post.id} />
<textarea name="body" placeholder="اكتب تعليقًا..." rows={3} required />
<button type="submit">إرسال</button>
</form>
</div>
)
}
prisma.post.findUnique) وكتابة Server Action (addComment) في صفحة واحدة، باستخدام إعداد Next.js + Prisma full-stack.
❓ أسئلة شائعة
globalThis، مما يضمن إنشاء PrismaClient واحد فقط.prisma migrate dev و prisma db push؟migrate dev ينشئ ملفات ترحيل SQL قابلة للتتبع (مناسبة للتعاون الجماعي)، بينما db push يزامن schema مباشرة مع قاعدة البيانات (مناسب للنماذج الأولية السريعة؛ لا يحتفظ بالسجل). يجب استخدام migrate deploy في بيئات الإنتاج.fetch).create: { post: { create: {...} } })، والتي تغلف المعاملات تلقائيًا. استخدم prisma.$transaction([...]) أو prisma.$transaction(async (tx) => {...}) عندما تحتاج إلى تعريف معاملة بشكل صريح.📖 ملخص
- يستخدم Prisma ORM Schema لتعريف النماذج بشكل تصريحي ويولد تلقائيًا عميل TypeScript آمن النوع
- ثلاثة نماذج للعلاقات: واحد إلى واحد، واحد إلى متعدد (مفتاح خارجي)، ومتعدد إلى متعدد (جدول وسيط ضمني)
- سير عمل الترحيل: تعديل Schema ←
migrate dev← توليد SQL تلقائيًا ← تحديث العميل - يوفر Prisma Studio واجهة رسومية في المتصفح لعرض وتحرير البيانات مباشرة
- يمكن استخدام عمليات CRUD بمرونة في RSC (قراءة)، و Server Action (كتابة)، و Route Handler (API)
- يمنع نمط singleton العام تسرب الاتصالات في بيئة التطوير؛ ويجب إضافة معلمات تجمع الاتصالات لبيئات serverless
📝 تمارين
-
تمرين أساسي (⭐): قم بتهيئة Prisma في مشروع موجود، وعرّف علاقة واحد إلى واحد بين نموذجي
UserوProfile، وشغل الترحيل، وأدخل سجلاً، وتحقق منه في Prisma Studio. -
تمرين متقدم (⭐⭐): نفذ مسار API كامل لـ CRUD لنظام المقالات — GET (قائمة + ترقيم الصفحات)، POST (إنشاء)، PATCH (تحديث)، و DELETE (حذف) — جميعها باستخدام عمليات Prisma.
-
تمرين تحدي (⭐⭐⭐): استخدم
$transactionمن Prisma لتنفيذ ميزة تسمح بإنشاء مقال وربطه بالوسوم في نفس الوقت. إذا كان الوسم غير موجود، قم بإنشاء الوسم أولاً، ثم أنشئ علاقة متعدد إلى متعدد بين المقال والوسم.