دمج قاعدة البيانات مع Prisma
مقدمة إلى Prisma ORM
Prisma هو ORM (ربط كائن-علاقة) حديث يجعل الوصول إلى قاعدة البيانات سهلاً وآمناً من حيث النوع لتطبيقات Next.js. يوفر واجهة برمجة تطبيقات بديهية، وترحيلات تلقائية، ودعم ممتاز لـ TypeScript. يغطي هذا الدرس التكامل الكامل لقاعدة البيانات مع Prisma.
تثبيت Prisma
لنقم بإعداد Prisma في مشروع Next.js الخاص بك:
# تثبيت Prisma CLI كتبعية تطوير npm install -D prisma # تثبيت Prisma Client npm install @prisma/client # تهيئة Prisma npx prisma init
هذا ينشئ مجلد prisma مع ملف schema.prisma ويضيف ملف .env لاتصال قاعدة البيانات الخاصة بك.
تكوين قاعدة البيانات
قم بتكوين اتصال قاعدة البيانات في ملف .env:
# .env # PostgreSQL DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public" # MySQL DATABASE_URL="mysql://user:password@localhost:3306/mydb" # SQLite (للتطوير) DATABASE_URL="file:./dev.db"
إنشاء المخطط الخاص بك
حدد نماذج البيانات الخاصة بك في prisma/schema.prisma:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
hashedPassword String?
image String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
comments Comment[]
sessions Session[]
accounts Account[]
@@map("users")
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String @db.Text
excerpt String?
published Boolean @default(false)
views Int @default(0)
authorId String
categoryId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
category Category? @relation(fields: [categoryId], references: [id])
comments Comment[]
tags PostTag[]
@@index([authorId])
@@index([categoryId])
@@index([slug])
@@map("posts")
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
description String?
createdAt DateTime @default(now())
posts Post[]
@@map("categories")
}
model Comment {
id String @id @default(cuid())
content String @db.Text
postId String
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@index([postId])
@@index([authorId])
@@map("comments")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
createdAt DateTime @default(now())
posts PostTag[]
@@map("tags")
}
model PostTag {
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
@@map("post_tags")
}
enum Role {
USER
ADMIN
MODERATOR
}
// نماذج Auth.js
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
تشغيل الترحيلات
بعد تعريف المخطط الخاص بك، أنشئ وشغل الترحيلات:
# إنشاء ترحيل npx prisma migrate dev --name init # هذا يفعل ثلاثة أشياء: # 1. ينشئ ملفات ترحيل SQL في prisma/migrations # 2. يطبق الترحيل على قاعدة البيانات # 3. يولد Prisma Client # تطبيق الترحيلات في الإنتاج npx prisma migrate deploy # إعادة تعيين قاعدة البيانات (التطوير فقط) npx prisma migrate reset
migrate reset في الإنتاج لأنه يحذف جميع البيانات. استخدم migrate deploy لعمليات النشر في الإنتاج.
إعداد Prisma Client
أنشئ نسخة Prisma Client يمكن إعادة استخدامها في تطبيقك:
<!-- lib/prisma.ts -->
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
عمليات CRUD الأساسية
لنقم بتنفيذ عمليات الإنشاء والقراءة والتحديث والحذف:
عمليات الإنشاء
<!-- lib/actions/post-actions.ts -->
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createPost(data: {
title: string
slug: string
content: string
excerpt?: string
authorId: string
categoryId?: string
published?: boolean
}) {
try {
const post = await prisma.post.create({
data: {
title: data.title,
slug: data.slug,
content: data.content,
excerpt: data.excerpt,
published: data.published ?? false,
author: {
connect: { id: data.authorId }
},
...(data.categoryId && {
category: {
connect: { id: data.categoryId }
}
})
},
include: {
author: {
select: {
id: true,
name: true,
email: true,
}
},
category: true,
}
})
revalidatePath('/blog')
return { success: true, post }
} catch (error) {
console.error('خطأ في إنشاء المنشور:', error)
return { success: false, error: 'فشل إنشاء المنشور' }
}
}
// الإنشاء مع العلاقات (متعدد إلى متعدد)
export async function createPostWithTags(data: {
title: string
slug: string
content: string
authorId: string
tagIds: string[]
}) {
try {
const post = await prisma.post.create({
data: {
title: data.title,
slug: data.slug,
content: data.content,
author: {
connect: { id: data.authorId }
},
tags: {
create: data.tagIds.map(tagId => ({
tag: {
connect: { id: tagId }
}
}))
}
},
include: {
tags: {
include: {
tag: true
}
}
}
})
revalidatePath('/blog')
return { success: true, post }
} catch (error) {
return { success: false, error: 'فشل إنشاء المنشور' }
}
}
عمليات القراءة
<!-- lib/queries/post-queries.ts -->
import { prisma } from '@/lib/prisma'
import { cache } from 'react'
// الحصول على منشور واحد (مخزن مؤقتاً)
export const getPostBySlug = cache(async (slug: string) => {
return await prisma.post.findUnique({
where: { slug },
include: {
author: {
select: {
id: true,
name: true,
email: true,
image: true,
}
},
category: true,
comments: {
include: {
author: {
select: {
id: true,
name: true,
image: true,
}
}
},
orderBy: {
createdAt: 'desc'
}
},
tags: {
include: {
tag: true
}
}
}
})
})
// الحصول على جميع المنشورات مع الترقيم والتصفية
export async function getPosts({
page = 1,
limit = 10,
categoryId,
authorId,
published = true,
searchQuery,
}: {
page?: number
limit?: number
categoryId?: string
authorId?: string
published?: boolean
searchQuery?: string
}) {
const skip = (page - 1) * limit
const where = {
...(published !== undefined && { published }),
...(categoryId && { categoryId }),
...(authorId && { authorId }),
...(searchQuery && {
OR: [
{ title: { contains: searchQuery, mode: 'insensitive' as const } },
{ content: { contains: searchQuery, mode: 'insensitive' as const } },
]
})
}
const [posts, total] = await Promise.all([
prisma.post.findMany({
where,
skip,
take: limit,
orderBy: {
createdAt: 'desc'
},
include: {
author: {
select: {
id: true,
name: true,
image: true,
}
},
category: true,
_count: {
select: {
comments: true
}
}
}
}),
prisma.post.count({ where })
])
return {
posts,
pagination: {
total,
pages: Math.ceil(total / limit),
currentPage: page,
perPage: limit,
}
}
}
// استعلامات التجميع
export async function getPostStats(authorId: string) {
const stats = await prisma.post.aggregate({
where: { authorId },
_count: {
id: true
},
_sum: {
views: true
},
_avg: {
views: true
}
})
return stats
}
عمليات التحديث
<!-- lib/actions/post-actions.ts -->
export async function updatePost(
id: string,
data: {
title?: string
slug?: string
content?: string
excerpt?: string
published?: boolean
categoryId?: string | null
}
) {
try {
const post = await prisma.post.update({
where: { id },
data: {
...(data.title && { title: data.title }),
...(data.slug && { slug: data.slug }),
...(data.content && { content: data.content }),
...(data.excerpt !== undefined && { excerpt: data.excerpt }),
...(data.published !== undefined && { published: data.published }),
...(data.categoryId !== undefined && {
category: data.categoryId
? { connect: { id: data.categoryId } }
: { disconnect: true }
})
}
})
revalidatePath('/blog')
revalidatePath(`/blog/${post.slug}`)
return { success: true, post }
} catch (error) {
return { success: false, error: 'فشل تحديث المنشور' }
}
}
// التحديث مع العلاقات
export async function updatePostTags(postId: string, tagIds: string[]) {
try {
await prisma.post.update({
where: { id: postId },
data: {
tags: {
deleteMany: {}, // إزالة جميع العلامات الموجودة
create: tagIds.map(tagId => ({
tag: {
connect: { id: tagId }
}
}))
}
}
})
revalidatePath('/blog')
return { success: true }
} catch (error) {
return { success: false, error: 'فشل تحديث العلامات' }
}
}
// زيادة عدد المشاهدات
export async function incrementPostViews(slug: string) {
await prisma.post.update({
where: { slug },
data: {
views: {
increment: 1
}
}
})
}
عمليات الحذف
<!-- lib/actions/post-actions.ts -->
export async function deletePost(id: string) {
try {
await prisma.post.delete({
where: { id }
})
revalidatePath('/blog')
return { success: true }
} catch (error) {
return { success: false, error: 'فشل حذف المنشور' }
}
}
// الحذف الناعم (باستخدام حقل deletedAt)
export async function softDeletePost(id: string) {
try {
await prisma.post.update({
where: { id },
data: {
deletedAt: new Date()
}
})
revalidatePath('/blog')
return { success: true }
} catch (error) {
return { success: false, error: 'فشل حذف المنشور' }
}
}
// الحذف الجماعي
export async function deletePostsByAuthor(authorId: string) {
try {
const result = await prisma.post.deleteMany({
where: { authorId }
})
revalidatePath('/blog')
return { success: true, count: result.count }
} catch (error) {
return { success: false, error: 'فشل حذف المنشورات' }
}
}
المعاملات
استخدم المعاملات لضمان نجاح أو فشل العمليات المتعددة معاً:
export async function createUserWithProfile(data: {
email: string
name: string
password: string
bio?: string
}) {
try {
const hashedPassword = await bcrypt.hash(data.password, 12)
const result = await prisma.$transaction(async (tx) => {
// إنشاء المستخدم
const user = await tx.user.create({
data: {
email: data.email,
name: data.name,
hashedPassword,
}
})
// إنشاء الملف الشخصي
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: data.bio || ''
}
})
// إنشاء الإعدادات الافتراضية
const settings = await tx.settings.create({
data: {
userId: user.id,
theme: 'light',
emailNotifications: true,
}
})
return { user, profile, settings }
})
return { success: true, data: result }
} catch (error) {
return { success: false, error: 'فشل إنشاء المستخدم' }
}
}
الاستعلامات المتقدمة
// التصفية المعقدة مع العلاقات المتداخلة
export async function searchPosts(query: string) {
return await prisma.post.findMany({
where: {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } },
{
author: {
name: { contains: query, mode: 'insensitive' }
}
},
{
tags: {
some: {
tag: {
name: { contains: query, mode: 'insensitive' }
}
}
}
}
],
published: true,
},
include: {
author: true,
category: true,
tags: {
include: { tag: true }
}
},
orderBy: [
{ views: 'desc' },
{ createdAt: 'desc' }
],
take: 20
})
}
// SQL خام للاستعلامات المعقدة
export async function getPopularPostsByViews() {
return await prisma.$queryRaw`
SELECT p.*, u.name as author_name, COUNT(c.id) as comment_count
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
LEFT JOIN comments c ON p.id = c.post_id
WHERE p.published = true
GROUP BY p.id, u.name
ORDER BY p.views DESC, comment_count DESC
LIMIT 10
`
}
- أنشئ مخططاً لمدونة مع مستخدمين ومنشورات وفئات وعلامات وتعليقات
- قم بتنفيذ عمليات CRUD الكاملة للمنشورات مع معالجة الأخطاء المناسبة
- أضف الترقيم والتصفية إلى قائمة المنشورات
- أنشئ معاملة تنشر منشوراً وترسل إشعارات
- قم بتنفيذ وظيفة بحث تستعلم عبر حقول وعلاقات متعددة
- أضف عد المشاهدات الذي يزداد بشكل ذري بدون شروط السباق
Prisma Studio
يوفر Prisma Studio متصفح قاعدة بيانات مرئي:
# تشغيل Prisma Studio npx prisma studio # يفتح http://localhost:5555
أفضل الممارسات
- استخدم غلاف
cache()للاستعلامات التي يتم الوصول إليها بشكل متكرر في مكونات الخادم - استخدم دائماً
revalidatePath()بعد الطفرات لتحديث البيانات المخزنة مؤقتاً - أضف فهارس إلى الحقول التي يتم الاستعلام عنها بشكل متكرر لتحسين الأداء
- استخدم
selectلجلب الحقول المطلوبة فقط، مما يقلل نقل البيانات - قم بتنفيذ معالجة الأخطاء المناسبة وإرجاع رسائل سهلة الفهم للمستخدم
- استخدم المعاملات للعمليات التي يجب أن تنجح أو تفشل معاً
- لا تعرض أبداً Prisma Client في مكونات العميل - استخدم إجراءات الخادم
الخلاصة
توفر Prisma طريقة آمنة من حيث النوع وبديهية للعمل مع قواعد البيانات في Next.js. مع الترحيلات التلقائية، والدعم الممتاز لـ TypeScript، والتكامل السلس مع مكونات الخادم وإجراءات الخادم، فإن Prisma هي الحل المثالي لقاعدة البيانات لتطبيقات Next.js الحديثة.