Next.js: دمج قواعد البيانات: Prisma

آخر تحديث: 2026-08-26

Prisma هو أكثر ORM شيوعًا في نظام Node.js البيئي — يتيح لك كتابة نماذج البيانات بلغة TypeScript ويولد تلقائيًا عملاء قاعدة بيانات آمنين النوع.

1. ما ستتعلمه



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
// 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) عملية التثبيت

BASH
# 1. تثبيت Prisma CLI والعميل
npm install prisma @prisma/client --save-dev
# أو دفعة واحدة
npx prisma init --datasource-provider postgresql
100%
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 للعميل

TS
// 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
💡 نصيحة: إعادة التحميل الساخن في Next.js تنشئ نسخة PrismaClient جديدة كل مرة يتم فيها تحديث الصفحة. نمط singleton العام يعيد استخدام الاتصالات الموجودة في بيئة التطوير، مما يمنع أخطاء "عدد كبير جدًا من الاتصالات".

▶ مثال: التحقق من اتصال قاعدة البيانات

المخرجات:

TEXT 📖 للعرض فقط
تم تنفيذ عملية قاعدة البيانات بنجاح.
TS
// 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 })
  }
}
💻 المخرجات:

JSON
{ "status": "ok", "db": "connected" }

المخرجات:

TEXT 📖 للعرض فقط
{ "status": "ok", "db": "connected" }  ← JSON بمفتاحين: status ("ok") و db ("connected")


4. نمذجة بيانات Schema

(1) أنواع علاقات النماذج

100%
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
// 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

BASH
# تشغيل Prisma Studio (واجهة متصفح رسومية لعرض/تحرير البيانات)
npx prisma studio
TEXT 📖 للعرض فقط
1. شغل `npx prisma studio`
2. افتح في المتصفح http://localhost:5555
3. اختر من اليسار جداول User / Post / Comment / Tag
4. انقر "Add Record" لإضافة بيانات اختبارية
5. انقر "Save Changes" للحفظ
💡 نصيحة: يدعم Studio التحرير المباشر للبيانات المرتبطة؛ عند تحرير User، يمكنك رؤية Posts و Comments المرتبطة به.



5. ترحيل قاعدة البيانات (Migration)

(1) سير عمل الترحيل

الخطوة الأمر الوظيفة
تعديل Schema تحرير schema.prisma إضافة أو إزالة نماذج/حقول
إنشاء ترحيل npx prisma migrate dev --name add_user_role توليد ملف SQL للترحيل
تطبيق الترحيل تلقائي تحديث هيكل قاعدة البيانات
إعادة تعيين قاعدة البيانات npx prisma migrate reset مسح البيانات + إعادة الترحيل
توليد العميل npx prisma generate تحديث أنواع TypeScript

(2) هيكل ملفات الترحيل

BASH
prisma/migrations/
├── 20260706000001_init/
│   └── migration.sql          # إنشاء الجداول الأولية
├── 20260706000002_add_user_role/
│   └── migration.sql          # ALTER TABLE إضافة عمود role
└── migration_lock.toml        # قفل مزود قاعدة البيانات

▶ مثال: إضافة ترحيل الدور

BASH
npx prisma migrate dev --name add_role_enum

SQL المُنشأ:

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';

المخرجات:

TEXT 📖 للعرض فقط
تم تنفيذ العبارة (العبارات) بنجاح.
💡 نصيحة: prisma migrate dev يكتشف تلقائيًا تغييرات schema ويولد عبارات SQL المقابلة. لا حاجة لكتابة عبارات ALTER TABLE يدويًا.



6. عمليات CRUD العملية

(1) Server Component يقرأ البيانات

TSX
// 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: كتابة البيانات

TS
// 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

المخرجات:

TEXT 📖 للعرض فقط
ينفذ Server Action ويستدعي revalidatePath() لتحديث ذاكرة التخزين المؤقت للصفحة.
TSX
// 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>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
نموذج يحتوي على حقول إدخال وزر إرسال.
النص المرئي: نشر
TS
// تصحيح 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 كامل

TS
// 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 })
}
TS
// 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) تكوين معلمات تجمع الاتصالات

TS
// 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
⚠️ ملاحظة: في بيئات serverless (Vercel)، يكون عدد اتصالات قاعدة البيانات محدودًا. نوصي باستخدام Prisma Accelerate (وسيط تجمع الاتصالات) أو تجمع اتصالات Supabase (?pgbouncer=true).

▶ مثال: تكوين تجمع اتصالات Supabase

ENV
# .env — تجمع اتصالات Supabase
DATABASE_URL="postgresql://postgres:password@db.xxxxx.supabase.co:6543/postgres?pgbouncer=true&connection_limit=5"
TS
// تحسين 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
}

المخرجات:

TEXT 📖 للعرض فقط
تم تنفيذ عملية قاعدة البيانات بنجاح.


8. مثال كامل: تنفيذ شامل لنظام مدونة CRUD

TSX
// 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>
  )
}
💡 نصيحة: يوضح هذا المثال قراءة بيانات RSC (prisma.post.findUnique) وكتابة Server Action (addComment) في صفحة واحدة، باستخدام إعداد Next.js + Prisma full-stack.


❓ أسئلة شائعة

س أيهما أختار: Prisma أم Drizzle ORM؟
ج Prisma أسهل للمبتدئين (schema تصريحي + تصور Studio + ترحيل تلقائي)، بينما Drizzle أقرب إلى صيغة SQL ويقدم أداءً أفضل قليلاً. يستخدم هذا الدرس Prisma لأنه يمتلك أكبر نظام بيئي (43k⭐) وتوثيق شامل.
س لماذا نستخدم نمط singleton العام لإنشاء PrismaClient؟
ج في وضع تطوير Next.js، تؤدي إعادة التحميل الساخن إلى إنشاء نسخ جديدة بشكل متكرر، مما قد يتسبب في ارتفاع كبير في عدد اتصالات قاعدة البيانات. يقوم singleton العام بتخزين النسخة في globalThis، مما يضمن إنشاء PrismaClient واحد فقط.
س ما الفرق بين prisma migrate dev و prisma db push؟
ج migrate dev ينشئ ملفات ترحيل SQL قابلة للتتبع (مناسبة للتعاون الجماعي)، بينما db push يزامن schema مباشرة مع قاعدة البيانات (مناسب للنماذج الأولية السريعة؛ لا يحتفظ بالسجل). يجب استخدام migrate deploy في بيئات الإنتاج.
س هل سيؤدي الاستعلام المباشر من Server Component إلى مشاكل في الأداء؟
ج لا. نظرًا لأن RSC يعمل على الخادم، فإن الاستعلام المباشر لقاعدة البيانات يلغي رحلة HTTP ذهابًا وإيابًا مقارنة بالمرور عبر مسار API. هذا يعمل بشكل أفضل عند دمجه مع التخزين المؤقت لبيانات Next.js (التخزين المؤقت التلقائي لـ fetch).
س كيف أضمن أن عمليات قاعدة البيانات تتم ضمن معاملة (transaction)؟
ج يدعم Prisma الكتابة المتداخلة (create: { post: { create: {...} } })، والتي تغلف المعاملات تلقائيًا. استخدم prisma.$transaction([...]) أو prisma.$transaction(async (tx) => {...}) عندما تحتاج إلى تعريف معاملة بشكل صريح.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): قم بتهيئة Prisma في مشروع موجود، وعرّف علاقة واحد إلى واحد بين نموذجي User و Profile، وشغل الترحيل، وأدخل سجلاً، وتحقق منه في Prisma Studio.

  2. تمرين متقدم (⭐⭐): نفذ مسار API كامل لـ CRUD لنظام المقالات — GET (قائمة + ترقيم الصفحات)، POST (إنشاء)، PATCH (تحديث)، و DELETE (حذف) — جميعها باستخدام عمليات Prisma.

  3. تمرين تحدي (⭐⭐⭐): استخدم $transaction من Prisma لتنفيذ ميزة تسمح بإنشاء مقال وربطه بالوسوم في نفس الوقت. إذا كان الوسم غير موجود، قم بإنشاء الوسم أولاً، ثم أنشئ علاقة متعدد إلى متعدد بين المقال والوسم.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%