قاعدة بيانات Prisma
كان MegaShop يستخدم دائمًا بيانات وهمية—بيانات المنتجات المخزنة في مصفوفات JavaScript، والتي تُفقد عند إعادة التشغيل. يحتاج Charlie إلى قاعدة بيانات حقيقية. SQL الأصلي الذي كتبه Bob عرضة للأخطاء ويفتقر إلى أمان النوع. يوفر Prisma ORM استعلامات قاعدة بيانات آمنة بالنوع ويولّد أنواع TypeScript بسطر واحد فقط من الكود.
1. ما ستتعلمه
- تثبيت وتهيئة Prisma: schema.prisma + generate + migrate
- تصميم نموذج البيانات: العلاقات بين جداول Product وCategory وUser وOrder وCartItem
- تكامل Nuxt: اتصال Singleton في server/utils/prisma.ts
- عمليات CRUD: findMany/create/update/delete + المعاملات + التجميعات
- MegaShop: بحث مُصفّى لملايين المنتجات + معاملات الطلبات
2. قصة حقيقية لمهندس معماري
(1) نقطة الألم: البيانات الوهمية لا يمكن الاستمرار بها
بعد أن أعاد Bob تشغيل خادم تطوير MegaShop، اختفت جميع بيانات المنتجات. جميع المنتجات التي أضافتها Alice والطلبات التي قدمتها اختفت. الأسوأ من ذلك، لم تكن هناك قاعدة بيانات في بيئة الإنتاج—لا مكان لتخزين بيانات ملايين المنتجات.
(2) حل Prisma ORM
يوفر Prisma عمليات قاعدة بيانات آمنة بالنوع ويولّد تلقائيًا أنواع TypeScript:
// استعلام آمن بالنوع - بدون SQL خام
const products = await prisma.product.findMany({
where: { category: { slug: 'electronics' } },
take: 20,
skip: (page - 1) * 20
})
(3) الفوائد: أمان النوع + استمرارية البيانات
تُخزن بيانات المنتجات باستمرار، وتتمتع استعلامات API باستنتاج نوع كامل—لن يخطئ Bob في تهجئة اسم حقل مرة أخرى، وسيظل طلب Alice موجودًا بعد إعادة التشغيل.
3. تثبيت وتهيئة Prisma
(1) ▶ مثال: تثبيت Prisma
npm install prisma @prisma/client
npx prisma init
الناتج:
# تم تنفيذ الأمر بنجاح
(2) ▶ مثال: مخطط Prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
image String?
inStock Boolean @default(true)
rating Float @default(0)
reviewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
cartItems CartItem[]
@@index([categoryId])
@@index([slug])
@@index([price])
}
model Category {
id Int @id @default(autoincrement())
name String
slug String @unique
parentId Int?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
products Product[]
@@index([slug])
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String?
avatar String?
provider String @default("email")
role Role @default(CUSTOMER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
cartItems CartItem[]
@@index([email])
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
@@index([userId])
@@index([status])
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
order Order @relation(fields: [orderId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int
price Decimal @db.Decimal(10, 2)
@@index([orderId])
}
model CartItem {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int @default(1)
@@unique([userId, productId])
}
enum Role {
CUSTOMER
ADMIN
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
الناتج:
// التنفيذ ناجح
(1) مخطط ER لـ MegaShop
erDiagram
Product ||--o{ OrderItem : "مضمّن في"
Product ||--o{ CartItem : "مضاف إلى"
Product }o--|| Category : "ينتمي إلى"
Category ||--o{ Category : "أب-ابن"
User ||--o{ Order : "يقدّم"
User ||--o{ CartItem : "يمتلك"
Order ||--o{ OrderItem : "يحتوي على"
4. تكامل Nuxt
(1) ▶ مثال: اتصال Singleton لـ Prisma
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
// نمط Singleton - منع تعدد النسخ في التطوير
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
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
}
الناتج:
// التنفيذ ناجح
(2) ▶ مثال: عميل Prisma مُبنى مسبقًا في Nitro
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// توليد عميل Prisma قبل البناء
externals: {
inline: ['.prisma/client']
}
},
hooks: {
'build:before': async () => {
const { execSync } = await import('child_process')
execSync('npx prisma generate')
}
}
})
الناتج:
// التنفيذ ناجح
5. عمليات CRUD
(1) ▶ مثال: استعلامات مُصفّاة لقوائم المنتجات
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20
const category = query.category as string
const search = query.search as string
const minPrice = Number(query.minPrice) || 0
const maxPrice = Number(query.maxPrice) || Infinity
const where = {
AND: [
category ? { category: { slug: category } } : {},
search ? { name: { contains: search, mode: 'insensitive' as const } } : {},
{ price: { gte: minPrice } },
maxPrice < Infinity ? { price: { lte: maxPrice } } : {}
]
}
const [items, total] = await Promise.all([
prisma.product.findMany({
where,
skip: (page - 1) * limit,
take: limit,
include: { category: { select: { name: true, slug: true } } },
orderBy: { createdAt: 'desc' }
}),
prisma.product.count({ where })
])
return { items, total, page, limit }
})
الناتج:
// التنفيذ ناجح
(2) ▶ مثال: استعلام تفاصيل المنتج
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { select: { name: true, slug: true } }
}
})
if (!product) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
return product
})
الناتج:
// التنفيذ ناجح
(3) ▶ مثال: إنشاء طلب (معاملة)
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { items } = await readBody(event)
// معاملة: إنشاء طلب + تحديث المخزون + مسح السلة
const order = await prisma.$transaction(async (tx) => {
// حساب الإجمالي
let total = 0
const orderItems = []
for (const item of items) {
const product = await tx.product.findUnique({ where: { id: item.productId } })
if (!product || !product.inStock) {
throw createError({ statusCode: 400, message: `Product ${item.productId} unavailable` })
}
total += Number(product.price) * item.quantity
orderItems.push({
productId: product.id,
quantity: item.quantity,
price: product.price
})
}
// إنشاء الطلب
const newOrder = await tx.order.create({
data: {
userId,
total,
items: { create: orderItems }
},
include: { items: { include: { product: true } } }
})
// مسح سلة المستخدم
await tx.cartItem.deleteMany({ where: { userId } })
return newOrder
})
return { order, message: 'Order created successfully' }
})
الناتج:
// التنفيذ ناجح
(4) ▶ مثال: استعلام تجميعي—إحصائيات المنتجات
// server/api/admin/stats.get.ts
export default defineEventHandler(async () => {
const [
totalProducts,
totalUsers,
totalOrders,
revenue,
avgPrice
] = await Promise.all([
prisma.product.count(),
prisma.user.count(),
prisma.order.count(),
prisma.order.aggregate({
_sum: { total: true },
where: { status: 'PAID' }
}),
prisma.product.aggregate({
_avg: { price: true }
})
])
return {
totalProducts,
totalUsers,
totalOrders,
totalRevenue: revenue._sum.total || 0,
averagePrice: avgPrice._avg.price || 0
}
})
الناتج:
// التنفيذ ناجح
6. ترحيل قاعدة البيانات
(1) ▶ مثال: أمر ترحيل Prisma
# إنشاء ترحيل من تغييرات المخطط
npx prisma migrate dev --name init
# تطبيق الترحيلات في الإنتاج
npx prisma migrate deploy
# إعادة تعيين قاعدة البيانات (التطوير فقط!)
npx prisma migrate reset
# توليد عميل Prisma
npx prisma generate
# فتح Prisma Studio (متصفح قاعدة بيانات مرئي)
npx prisma studio
# بذر قاعدة البيانات ببيانات الاختبار
npx prisma db seed
الناتج:
# تم تنفيذ الأمر بنجاح
(2) ▶ مثال: بذر البيانات
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// إنشاء الفئات
const electronics = await prisma.category.create({
data: { name: 'Electronics', slug: 'electronics' }
})
const clothing = await prisma.category.create({
data: { name: 'Clothing', slug: 'clothing' }
})
// إنشاء المنتجات
for (let i = 1; i <= 1000; i++) {
await prisma.product.create({
data: {
name: `Product ${i}`,
slug: `product-${i}`,
price: Math.round(Math.random() * 500 * 100) / 100,
categoryId: i % 2 === 0 ? electronics.id : clothing.id,
inStock: Math.random() > 0.2,
rating: Math.round(Math.random() * 5 * 10) / 10,
reviewCount: Math.floor(Math.random() * 5000)
}
})
}
// إنشاء مستخدم مسؤول
await prisma.user.create({
data: {
email: 'bob@megashop.com',
name: 'Bob Admin',
role: 'ADMIN'
}
})
console.log('Seed completed: 1 thousand products + 2 categories + 1 admin')
}
main()
الناتج:
// التنفيذ ناجح
7. مثال شامل: تكامل قاعدة بيانات MegaShop
# .env
DATABASE_URL="postgresql://megashop:password@localhost:5432/megashop"
REDIS_URL="redis://localhost:6379"
JWT_ACCESS_SECRET="your-access-secret"
JWT_REFRESH_SECRET="your-refresh-secret"
// server/api/products/search.get.ts - بحث متقدم
export default defineEventHandler(async (event) => {
const { q, category, minPrice, maxPrice, sort, page, limit } = getQuery(event)
const where = {
AND: [
q ? { OR: [{ name: { contains: q as string, mode: 'insensitive' } }, { description: { contains: q as string, mode: 'insensitive' } }] } : {},
category ? { category: { slug: category as string } } : {},
minPrice ? { price: { gte: Number(minPrice) } } : {},
maxPrice ? { price: { lte: Number(maxPrice) } } : {}
]
}
const orderBy: any = sort === 'price-asc' ? { price: 'asc' }
: sort === 'price-desc' ? { price: 'desc' }
: sort === 'rating' ? { rating: 'desc' }
: { createdAt: 'desc' }
const [items, total] = await Promise.all([
prisma.product.findMany({
where, orderBy,
skip: ((Number(page) || 1) - 1) * (Number(limit) || 20),
take: Number(limit) || 20,
include: { category: { select: { name: true, slug: true } } }
}),
prisma.product.count({ where })
])
return { items, total, page: Number(page) || 1, limit: Number(limit) || 20 }
})
❓ أسئلة شائعة
findMany من Prisma مع skip/take يولّد ترقيم LIMIT/OFFSET، الذي يصبح أبطأ عندما تكون قيمة OFFSET كبيرة لمجموعات بيانات بملايين السجلات. لمجموعات البيانات الكبيرة، استخدم ترقيمًا قائمًا على المؤشر (cursor + take).prisma.$transaction؛ تُلتزم المعاملة فقط إذا نجحت جميع العمليات، وتُتراجع إذا فشلت أي عملية واحدة."prisma": { "seed": "npx ts-node prisma/seed.ts" } إلى package.json، ثم شغّل npx prisma db seed.📖 ملخص
- يقدم Prisma تعريفات المخطط والترحيل الآمن والاستعلامات الآمنة بالنوع وStudio مرئي
- ينفذ server/utils/prisma.ts اتصال Singleton لمنع تسرب الاتصالات في بيئة التطوير
- CRUD: findMany مع ترقيم + findUnique للتفاصيل + create + update + delete
- يضمن $transaction تنفيذ إنشاء الطلبات وتحديث المخزون ومسح سلة التسوق بشكل ذري
- مجموعات بيانات بملايين السجلات: تحسين الفهرسة + ترقيم بالمؤشر + استعلامات تجميعية للإحصائيات
📝 تمارين
- تمرين أساسي (الصعوبة ⭐): تثبيت Prisma وPostgreSQL، وإنشاء نموذجي Product وCategory، وتشغيل
migrateوseed. - تمرين متقدم (الصعوبة: ⭐⭐): تنفيذ واجهة CRUD كاملة للمنتجات، استبدال البيانات الوهمية السابقة، ودعم الترقيم والتصفية حسب الفئة.
- تحدي (الصعوبة: ⭐⭐⭐): تنفيذ معاملة طلب—إنشاء طلب، خصم المخزون، ومسح سلة التسوق—جميعها ضمن معاملة واحدة؛ إذا فشلت أي خطوة، تراجع عن المعاملة بالكامل.
---|



