بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج

Noqta TeamAI Bot
بواسطة Noqta Team & AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

SQL آمن الأنواع يبدو مثل TypeScript. Drizzle ORM هو الـ ORM الحديث والخفيف الذي يمنحك كامل قوة SQL بدون أي عبء إضافي أثناء التشغيل. في هذا الدليل، ستبني تطبيقاً كاملاً لإدارة المهام باستخدام Next.js 15 وServer Actions وPostgreSQL.

ما ستتعلمه

بنهاية هذا الدليل، ستكون قادراً على:

  • إعداد Drizzle ORM مع PostgreSQL في مشروع Next.js 15
  • تعريف مخطط قاعدة بيانات آمن الأنواع بلغة TypeScript الصرفة
  • تشغيل الترحيلات باستخدام drizzle-kit
  • بناء عمليات CRUD كاملة باستخدام Next.js Server Actions
  • التعامل مع إرسال النماذج باستخدام useActionState والتحقق بـ Zod
  • تنفيذ استعلامات علائقية مع منشئ استعلامات Drizzle
  • نشر تطبيق جاهز للإنتاج مع أنماط قاعدة بيانات صحيحة

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبّت (node --version)
  • خبرة في TypeScript (الأنواع، الأنواع المعممة، async/await)
  • معرفة بـ Next.js (App Router، Server Components)
  • PostgreSQL يعمل محلياً أو قاعدة بيانات سحابية (سنستخدم Neon)
  • محرر أكواد — يُنصح بـ VS Code أو Cursor

لماذا Drizzle ORM؟

يضم نظام JavaScript/TypeScript عدة أدوات ORM — مثل Prisma وTypeORM وSequelize — فلماذا نختار Drizzle؟ إليك ما يميزه:

الميزةDrizzlePrismaTypeORM
حجم الحزمة~7.4 KB~280 KB~180 KB
لغة المخططTypeScriptDSL مخصص (.prisma)TypeScript/مزخرفات
التحكم في SQLواجهة شبيهة بـ SQLاستعلامات مجردةاستعلامات مجردة
جاهز للحوسبة بدون خادمنعم (بدون تبعيات)يتطلب ملف محرك ثنائيغير محسّن
أمان الأنواعاستنتاج كاملأنواع مولّدةجزئي
منحنى التعلمتعرف SQL = تعرف Drizzleبناء جمل جديدأنماط المزخرفات

Drizzle يتبع نهجاً مختلفاً جذرياً: إذا كنت تعرف SQL، فأنت تعرف Drizzle بالفعل. لا توجد لغة استعلام مخصصة، ولا خطوة لتوليد الأكواد، ولا عبء أثناء التشغيل. مخططك هو TypeScript، واستعلاماتك تبدو مثل SQL، وكل نوع إرجاع يُستنتج تلقائياً.


الخطوة 1: إنشاء مشروع Next.js 15 جديد

ابدأ بإنشاء هيكل مشروع Next.js جديد مع TypeScript:

npx create-next-app@latest task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd task-manager

اختر الإعدادات الافتراضية عند السؤال. هذا يمنحك مشروع Next.js 15 مع:

  • App Router
  • TypeScript
  • Tailwind CSS
  • بنية مجلد src/

الخطوة 2: تثبيت Drizzle ORM والتبعيات

ثبّت Drizzle ORM ومشغل PostgreSQL وDrizzle Kit (أداة سطر الأوامر للترحيلات):

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

نستخدم @neondatabase/serverless كمشغل PostgreSQL لأنه يعمل بسلاسة في بيئات الحوسبة بدون خادم والبيئات التقليدية. إذا كنت تستخدم PostgreSQL محلي، يمكنك استخدام postgres (postgres.js) أو pg بدلاً من ذلك:

# بديل: لـ PostgreSQL المحلي
npm install drizzle-orm postgres
npm install -D drizzle-kit

الخطوة 3: إعداد اتصال قاعدة البيانات

أنشئ ملف .env.local مع سلسلة الاتصال بقاعدة البيانات:

DATABASE_URL="postgresql://username:password@hostname/database?sslmode=require"

تستخدم Neon؟ سجّل في neon.tech، أنشئ مشروعاً، وانسخ سلسلة الاتصال من لوحة التحكم. الطبقة المجانية تمنحك 512 ميجابايت — أكثر من كافٍ لهذا الدليل.

الآن أنشئ ملف اتصال قاعدة البيانات:

// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
 
const sql = neon(process.env.DATABASE_URL!);
 
export const db = drizzle({ client: sql, schema });

لـ PostgreSQL المحلي مع postgres (postgres.js)، الإعداد يبدو هكذا:

// src/db/index.ts (بديل لـ PostgreSQL المحلي)
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
 
const client = postgres(process.env.DATABASE_URL!);
 
export const db = drizzle({ client, schema });

الخطوة 4: تعريف مخطط قاعدة البيانات

هنا يتألق Drizzle. مخططك هو TypeScript خالص — بدون DSL مخصص، بدون مزخرفات، فقط دوال وأنواع:

// src/db/schema.ts
import {
  pgTable,
  serial,
  text,
  boolean,
  timestamp,
  integer,
  pgEnum,
} from "drizzle-orm/pg-core";
 
// تعريف تعداد لأولوية المهمة
export const priorityEnum = pgEnum("priority", ["low", "medium", "high", "urgent"]);
 
// جدول المستخدمين
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
 
// جدول المشاريع
export const projects = pgTable("projects", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  description: text("description"),
  userId: integer("user_id")
    .references(() => users.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
 
// جدول المهام
export const tasks = pgTable("tasks", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  description: text("description"),
  completed: boolean("completed").default(false).notNull(),
  priority: priorityEnum("priority").default("medium").notNull(),
  projectId: integer("project_id")
    .references(() => projects.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

لاحظ كيف يُقرأ هذا تقريباً مثل تعليمات SQL CREATE TABLE، لكن مع استنتاج أنواع TypeScript الكامل. كل نوع عمود وقيد وعلاقة محدد الأنواع بقوة.

تعريف العلاقات

يفصل Drizzle المعلومات العلائقية عن مخطط الجدول. هذا يبقي الأمور صريحة:

// src/db/schema.ts (تابع)
import { relations } from "drizzle-orm";
 
export const usersRelations = relations(users, ({ many }) => ({
  projects: many(projects),
}));
 
export const projectsRelations = relations(projects, ({ one, many }) => ({
  user: one(users, {
    fields: [projects.userId],
    references: [users.id],
  }),
  tasks: many(tasks),
}));
 
export const tasksRelations = relations(tasks, ({ one }) => ({
  project: one(projects, {
    fields: [tasks.projectId],
    references: [projects.id],
  }),
}));

الخطوة 5: إعداد Drizzle Kit

أنشئ ملف إعداد Drizzle Kit في جذر المشروع:

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
 
export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

أضف سكريبتات الترحيل إلى ملف package.json:

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
}

الخطوة 6: توليد وتشغيل الترحيلات

ولّد ملفات الترحيل من مخططك:

npm run db:generate

هذا ينشئ ملفات ترحيل SQL في مجلد drizzle/. افحصها لترى بالضبط أي SQL سيُنفّذ — Drizzle لا يخفي SQL عنك أبداً.

طبّق الترحيلات على قاعدة بياناتك:

npm run db:migrate

نصيحة للتطوير السريع: استخدم npm run db:push أثناء التطوير لدفع تغييرات المخطط مباشرة بدون توليد ملفات ترحيل. استخدم db:generate + db:migrate لنشر الإنتاج.


الخطوة 7: بناء Server Actions آمنة الأنواع

لنبنِ الآن عمليات CRUD الأساسية باستخدام Next.js Server Actions. هذه تعمل على الخادم ويمكن استدعاؤها مباشرة من مكونات React.

إجراء إنشاء مهمة

// src/app/actions/tasks.ts
"use server";
 
import { db } from "@/db";
import { tasks } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
 
// مخطط التحقق
const createTaskSchema = z.object({
  title: z.string().min(1, "العنوان مطلوب").max(200),
  description: z.string().optional(),
  priority: z.enum(["low", "medium", "high", "urgent"]),
  projectId: z.coerce.number().positive(),
});
 
export type ActionState = {
  message: string;
  errors?: Record<string, string[]>;
  success?: boolean;
};
 
export async function createTask(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  const validatedFields = createTaskSchema.safeParse({
    title: formData.get("title"),
    description: formData.get("description"),
    priority: formData.get("priority"),
    projectId: formData.get("projectId"),
  });
 
  if (!validatedFields.success) {
    return {
      message: "فشل التحقق",
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }
 
  try {
    await db.insert(tasks).values(validatedFields.data);
    revalidatePath("/dashboard");
    return { message: "تم إنشاء المهمة بنجاح", success: true };
  } catch (error) {
    return { message: "فشل إنشاء المهمة. يرجى المحاولة مرة أخرى." };
  }
}

تبديل حالة إكمال المهمة

// src/app/actions/tasks.ts (تابع)
 
export async function toggleTask(taskId: number) {
  const [task] = await db
    .select({ completed: tasks.completed })
    .from(tasks)
    .where(eq(tasks.id, taskId));
 
  if (!task) return;
 
  await db
    .update(tasks)
    .set({
      completed: !task.completed,
      updatedAt: new Date(),
    })
    .where(eq(tasks.id, taskId));
 
  revalidatePath("/dashboard");
}

حذف مهمة

// src/app/actions/tasks.ts (تابع)
 
export async function deleteTask(taskId: number) {
  await db.delete(tasks).where(eq(tasks.id, taskId));
  revalidatePath("/dashboard");
}

الخطوة 8: بناء مكون نموذج المهام

أنشئ مكون نموذج يستخدم useActionState للتكامل السلس مع server action:

// src/components/TaskForm.tsx
"use client";
 
import { useActionState } from "react";
import { createTask, type ActionState } from "@/app/actions/tasks";
 
const initialState: ActionState = { message: "" };
 
export function TaskForm({ projectId }: { projectId: number }) {
  const [state, formAction, pending] = useActionState(createTask, initialState);
 
  return (
    <form action={formAction} className="space-y-4">
      <input type="hidden" name="projectId" value={projectId} />
 
      <div>
        <label htmlFor="title" className="block text-sm font-medium">
          عنوان المهمة
        </label>
        <input
          type="text"
          id="title"
          name="title"
          required
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
          placeholder="ما الذي يجب إنجازه؟"
        />
        {state.errors?.title && (
          <p className="mt-1 text-sm text-red-600">{state.errors.title[0]}</p>
        )}
      </div>
 
      <div>
        <label htmlFor="description" className="block text-sm font-medium">
          الوصف (اختياري)
        </label>
        <textarea
          id="description"
          name="description"
          rows={3}
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
          placeholder="أضف المزيد من التفاصيل..."
        />
      </div>
 
      <div>
        <label htmlFor="priority" className="block text-sm font-medium">
          الأولوية
        </label>
        <select
          id="priority"
          name="priority"
          defaultValue="medium"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        >
          <option value="low">منخفضة</option>
          <option value="medium">متوسطة</option>
          <option value="high">عالية</option>
          <option value="urgent">عاجلة</option>
        </select>
      </div>
 
      <button
        type="submit"
        disabled={pending}
        className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {pending ? "جارٍ الإنشاء..." : "إنشاء مهمة"}
      </button>
 
      {state.message && !state.errors && (
        <p
          className={`text-sm ${state.success ? "text-green-600" : "text-red-600"}`}
          aria-live="polite"
        >
          {state.message}
        </p>
      )}
    </form>
  );
}

الخطوة 9: بناء لوحة التحكم مع الاستعلامات العلائقية

لنجلب الآن البيانات باستخدام واجهة الاستعلامات العلائقية القوية في Drizzle:

// src/app/dashboard/page.tsx
import { db } from "@/db";
import { TaskForm } from "@/components/TaskForm";
import { TaskList } from "@/components/TaskList";
 
export default async function DashboardPage() {
  // استعلام علائقي: جلب المشاريع مع مهامها
  const projectsWithTasks = await db.query.projects.findMany({
    with: {
      tasks: {
        orderBy: (tasks, { desc }) => [desc(tasks.createdAt)],
      },
    },
    orderBy: (projects, { desc }) => [desc(projects.createdAt)],
  });
 
  return (
    <main className="mx-auto max-w-4xl p-8">
      <h1 className="mb-8 text-3xl font-bold">مدير المهام</h1>
 
      {projectsWithTasks.map((project) => (
        <section key={project.id} className="mb-8 rounded-lg border p-6">
          <h2 className="mb-4 text-xl font-semibold">{project.name}</h2>
          <p className="mb-4 text-gray-600">{project.description}</p>
 
          <TaskList tasks={project.tasks} />
          <TaskForm projectId={project.id} />
        </section>
      ))}
    </main>
  );
}

الخطوة 10: استعلامات متقدمة مع Drizzle

يدعم Drizzle عمليات SQL المعقدة مع أمان أنواع كامل. إليك بعض الأنماط التي ستستخدمها بشكل متكرر:

استعلامات مفلترة مع WHERE

import { eq, and, or, like, desc, asc, count, sql } from "drizzle-orm";
 
// جلب المهام غير المكتملة ذات الأولوية العالية
const urgentTasks = await db
  .select()
  .from(tasks)
  .where(
    and(
      eq(tasks.completed, false),
      or(eq(tasks.priority, "high"), eq(tasks.priority, "urgent"))
    )
  )
  .orderBy(desc(tasks.createdAt));
 
// البحث في المهام بالعنوان
const searchResults = await db
  .select()
  .from(tasks)
  .where(like(tasks.title, `%${searchTerm}%`));

التجميعات

// حساب المهام لكل مشروع
const taskCounts = await db
  .select({
    projectId: tasks.projectId,
    projectName: projects.name,
    total: count(),
    completed: count(sql`CASE WHEN ${tasks.completed} THEN 1 END`),
  })
  .from(tasks)
  .innerJoin(projects, eq(tasks.projectId, projects.id))
  .groupBy(tasks.projectId, projects.name);

المعاملات (Transactions)

// إنشاء مشروع مع مهام أولية بشكل ذري
import { db } from "@/db";
 
await db.transaction(async (tx) => {
  const [project] = await tx
    .insert(projects)
    .values({ name: "مشروع جديد", userId: 1 })
    .returning();
 
  await tx.insert(tasks).values([
    { title: "إعداد المستودع", projectId: project.id, priority: "high" },
    { title: "كتابة التوثيق", projectId: project.id, priority: "medium" },
    { title: "النشر في الإنتاج", projectId: project.id, priority: "low" },
  ]);
});

الخطوة 11: الاستكشاف باستخدام Drizzle Studio

Drizzle Studio هو واجهة رسومية مدمجة لقاعدة البيانات تتيح لك تصفح البيانات وتحريرها بصرياً:

npm run db:studio

هذا يفتح واجهة ويب محلية على https://local.drizzle.studio حيث يمكنك:

  • تصفح جميع الجداول والبيانات
  • تعديل السجلات مباشرة
  • تشغيل استعلامات SQL خام
  • عرض علاقات الجداول بصرياً

الخطوة 12: ملء قاعدة البيانات ببيانات أولية

أنشئ سكريبت لملء قاعدة بياناتك ببيانات نموذجية:

// src/db/seed.ts
import { db } from "./index";
import { users, projects, tasks } from "./schema";
 
async function seed() {
  console.log("جارٍ ملء قاعدة البيانات...");
 
  // إنشاء مستخدم
  const [user] = await db
    .insert(users)
    .values({ name: "أحمد محمد", email: "ahmed@example.com" })
    .returning();
 
  // إنشاء مشاريع
  const [project1] = await db
    .insert(projects)
    .values([
      { name: "إعادة تصميم الموقع", description: "تجديد موقع الشركة", userId: user.id },
      { name: "تطبيق الهاتف", description: "بناء تطبيق React Native", userId: user.id },
    ])
    .returning();
 
  // إنشاء مهام
  await db.insert(tasks).values([
    { title: "تصميم النماذج الأولية", priority: "high", projectId: project1.id },
    { title: "تنفيذ الصفحة الرئيسية", priority: "medium", projectId: project1.id },
    { title: "إضافة الوضع المظلم", priority: "low", projectId: project1.id },
    { title: "كتابة الاختبارات", priority: "high", projectId: project1.id },
  ]);
 
  console.log("اكتمل ملء قاعدة البيانات!");
}
 
seed().catch(console.error);

شغّله:

npm run db:seed

اختبار التطبيق

شغّل خادم التطوير وتحقق من عمل كل شيء:

npm run dev

افتح http://localhost:3000/dashboard واختبر:

  1. عرض المشاريع والمهام — البيانات تُحمّل من قاعدة البيانات
  2. إنشاء مهمة جديدة — النموذج يُرسل عبر Server Action، والصفحة تُعاد التحقق منها
  3. تبديل الإكمال — مربع الاختيار يُحدّث قاعدة البيانات والواجهة
  4. حذف مهمة — العنصر يُزال من قاعدة البيانات والقائمة
  5. فحص Drizzle Studio — شغّل npm run db:studio جنباً إلى جنب لمشاهدة التغييرات بالوقت الفعلي

استكشاف الأخطاء وإصلاحها

"Cannot find module '@/db'"

تأكد أن ملف tsconfig.json لديه المسار البديل الصحيح:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

"relation does not exist"

تحتاج لتشغيل الترحيلات أولاً. شغّل npm run db:push (للتطوير) أو npm run db:generate && npm run db:migrate لإنشاء الجداول.

"Type error: Column type mismatch"

هذا يعني عادة أن مخطط TypeScript انحرف عن قاعدة البيانات الفعلية. شغّل npm run db:generate لإنشاء ترحيل جديد يوفّق بين الاختلافات.


الخطوات التالية

لديك الآن أساس قوي لبناء تطبيقات متكاملة آمنة الأنواع مع Drizzle ORM و Next.js 15. إليك ما يمكنك فعله بعد ذلك:

  • إضافة المصادقة — ادمج Better Auth أو NextAuth.js مع Drizzle كمحول قاعدة البيانات
  • إضافة التحديثات الفورية — استخدم Drizzle مع WebSockets أو server-sent events
  • تنفيذ واجهة متفائلة — ادمج useOptimistic مع Server Actions للاستجابة الفورية
  • إضافة البحث والتصفية — استخدم عوامل like وilike والبحث النصي الكامل في Drizzle
  • النشر على Vercel — يعمل فوراً مع Neon PostgreSQL
  • استكشف توثيق Drizzle للميزات المتقدمة

الخلاصة

يمثل Drizzle ORM فلسفة جديدة في أدوات TypeScript ORM: SQL ليس شيئاً نختبئ منه — إنه شيء نتبناه مع أمان الأنواع. على عكس أدوات ORM التي تخترع لغات استعلام خاصة بها، يرتبط Drizzle مباشرة بمفاهيم SQL التي تعرفها بالفعل مع منحك كامل قوة نظام أنواع TypeScript.

في هذا الدليل، بنيت تطبيقاً كاملاً لإدارة المهام يوضح:

  • المخطط ككود — بنية قاعدة بياناتك تعيش في TypeScript
  • بدون عبء تشغيل — Drizzle يُترجم إلى SQL فعال
  • أمان أنواع كامل — من المخطط إلى نتيجة الاستعلام، كل نوع مُستنتج
  • أنماط حديثة — Server Actions وuseActionState والتحقق بـ Zod
  • تجربة مطور ممتازة — Drizzle Studio والترحيلات وواجهة SQL أولاً

مزيج Drizzle ORM + Next.js 15 + PostgreSQL يمنحك مكدساً جاهزاً للإنتاج، خفيفاً وآمن الأنواع وممتعاً للعمل معه. ابدأ البناء.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على دمج نماذج التفكير من OpenAI في طلبات السحب على GitHub.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

30 د قراءة·