404 Not Found

404 Not Found


nginx

قاعدة بيانات Prisma

كان MegaShop يستخدم دائمًا بيانات وهمية—بيانات المنتجات المخزنة في مصفوفات JavaScript، والتي تُفقد عند إعادة التشغيل. يحتاج Charlie إلى قاعدة بيانات حقيقية. SQL الأصلي الذي كتبه Bob عرضة للأخطاء ويفتقر إلى أمان النوع. يوفر Prisma ORM استعلامات قاعدة بيانات آمنة بالنوع ويولّد أنواع TypeScript بسطر واحد فقط من الكود.

1. ما ستتعلمه


2. قصة حقيقية لمهندس معماري

(1) نقطة الألم: البيانات الوهمية لا يمكن الاستمرار بها

بعد أن أعاد Bob تشغيل خادم تطوير MegaShop، اختفت جميع بيانات المنتجات. جميع المنتجات التي أضافتها Alice والطلبات التي قدمتها اختفت. الأسوأ من ذلك، لم تكن هناك قاعدة بيانات في بيئة الإنتاج—لا مكان لتخزين بيانات ملايين المنتجات.

(2) حل Prisma ORM

يوفر Prisma عمليات قاعدة بيانات آمنة بالنوع ويولّد تلقائيًا أنواع TypeScript:

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

BASH
npm install prisma @prisma/client
npx prisma init

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

(2) ▶ مثال: مخطط Prisma

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
}

الناتج:

TEXT
// التنفيذ ناجح

(1) مخطط ER لـ MegaShop

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

TYPESCRIPT
// 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
}

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: عميل Prisma مُبنى مسبقًا في Nitro

TYPESCRIPT
// 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')
    }
  }
})

الناتج:

TEXT
// التنفيذ ناجح

5. عمليات CRUD

(1) ▶ مثال: استعلامات مُصفّاة لقوائم المنتجات

TYPESCRIPT
// 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 }
})

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: استعلام تفاصيل المنتج

TYPESCRIPT
// 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
})

الناتج:

TEXT
// التنفيذ ناجح

(3) ▶ مثال: إنشاء طلب (معاملة)

TYPESCRIPT
// 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' }
})

الناتج:

TEXT
// التنفيذ ناجح

(4) ▶ مثال: استعلام تجميعي—إحصائيات المنتجات

TYPESCRIPT
// 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
  }
})

الناتج:

TEXT
// التنفيذ ناجح

6. ترحيل قاعدة البيانات

(1) ▶ مثال: أمر ترحيل Prisma

BASH
# إنشاء ترحيل من تغييرات المخطط
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

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

(2) ▶ مثال: بذر البيانات

TYPESCRIPT
// 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()

الناتج:

TEXT
// التنفيذ ناجح

7. مثال شامل: تكامل قاعدة بيانات MegaShop

TEXT
# .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"
TYPESCRIPT
// 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 }
})

❓ أسئلة شائعة

س ما الفرق بين Prisma وTypeORM؟
ج يستخدم Prisma مخططًا تعريفيًا ويولّد كودًا من جانب العميل، مما يوفر استنتاج نوع أكثر شمولاً. يستخدم TypeORM المصممات وأقرب إلى ORM تقليدي. مجتمع Nuxt 3 يوصي بـ Prisma.
س لماذا يحتاج Singleton لـ Prisma إلى متغير عام؟
ج في بيئة التطوير، تعيد إعادة تحميل Nuxt تنفيذ كود الخادم؛ بدون متغير عام، سيتم إنشاء نسخ متعددة من PrismaClient، مما يؤدي إلى تسرب الاتصالات. هذه المشكلة غير موجودة في بيئة الإنتاج.
س كيف يكون أداء الاستعلام مع ملايين السجلات؟
ج findMany من Prisma مع skip/take يولّد ترقيم LIMIT/OFFSET، الذي يصبح أبطأ عندما تكون قيمة OFFSET كبيرة لمجموعات بيانات بملايين السجلات. لمجموعات البيانات الكبيرة، استخدم ترقيمًا قائمًا على المؤشر (cursor + take).
س هل يدعم Prisma قاعدة بيانات MongoDB؟
ج نعم، لكنها ميزة معاينة. PostgreSQL هو الأكثر توافقًا مع Prisma ويوفر أكمل مجموعة من الميزات. يوصي MegaShop بـ PostgreSQL.
س هل يمكن إجراء عمليات كتابة متعددة ضمن معاملة واحدة؟
ج نعم. يمكنك إجراء أي عدد من عمليات القراءة والكتابة ضمن prisma.$transaction؛ تُلتزم المعاملة فقط إذا نجحت جميع العمليات، وتُتراجع إذا فشلت أي عملية واحدة.
س كيف أشغل نص البذر؟
ج أضف "prisma": { "seed": "npx ts-node prisma/seed.ts" } إلى package.json، ثم شغّل npx prisma db seed.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): تثبيت Prisma وPostgreSQL، وإنشاء نموذجي Product وCategory، وتشغيل migrate وseed.
  2. تمرين متقدم (الصعوبة: ⭐⭐): تنفيذ واجهة CRUD كاملة للمنتجات، استبدال البيانات الوهمية السابقة، ودعم الترقيم والتصفية حسب الفئة.
  3. تحدي (الصعوبة: ⭐⭐⭐): تنفيذ معاملة طلب—إنشاء طلب، خصم المخزون، ومسح سلة التسوق—جميعها ضمن معاملة واحدة؛ إذا فشلت أي خطوة، تراجع عن المعاملة بالكامل.

---|

Web-Tutorial.com

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

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

100%