إنتقل إلى المحتوى الرئيسي

تطوير Drizzle ORM في Cursor: دليل شامل

تطوير Drizzle في Cursor

Drizzle ORM هو ORM خفيف الوزن يضع TypeScript أولاً يوفر استعلامات SQL آمنة الأنواع دون عبء وقت تشغيل ثقيل. على عكس ORMs التقليدية، يمنحك Drizzle تحكماً كاملاً في SQL مع الحفاظ على أمان أنواع كامل في وقت التجميع. يدعم Drizzle PostgreSQL و MySQL و SQLite بواجهة برمجة موحدة. يُعد Cursor، بمساعد الذكاء الاصطناعي المدمج وخادم لغة TypeScript المدمج، محرراً مثالياً لتطوير Drizzle -- فهو يوفر IntelliSense كاملاً لتعريفات المخطط، ويُكمل تلقائياً منشئي الاستعلامات، ويمكنه إنشاء مخططات واستعلامات كاملة من أوصاف اللغة الطبيعية.

يغطي هذا الدليل سير عمل تطوير Drizzle الكامل في Cursor: التثبيت والإعداد، تعريف المخطط، التهجرات مع drizzle-kit، الاستعلامات الآمنة الأنواع، العلاقات، وبناء الاستعلامات بمساعدة الذكاء الاصطناعي.

تثبيت وإعداد Drizzle في Cursor​

المتطلبات الأساسية​

يتطلب Drizzle إصدار Node.js 18 أو أحدث ويعمل مع PostgreSQL أو MySQL أو SQLite. تحقق من إصدار Node.js:

node --version
تلميح

يوفر خادم لغة TypeScript المدمج في Cursor فحص أنواع في الوقت الفعلي لمخططات واستعلامات Drizzle. ترى أخطاء الأنواع مضمنة قبل تشغيل الكود حتى، وهو ذو قيمة خاصة لمنشئي استعلامات Drizzle ذوي الأنواع الكثيفة.

تثبيت التبعيات​

ثبّت Drizzle ORM وأداة drizzle-kit CLI:

# For PostgreSQL
npm install drizzle-orm pg
npm install drizzle-kit --save-dev

# For MySQL
npm install drizzle-orm mysql2
npm install drizzle-kit --save-dev

# For SQLite (better-sqlite3)
npm install drizzle-orm better-sqlite3
npm install drizzle-kit --save-dev

# For SQLite (libSQL/Turso)
npm install drizzle-orm @libsql/client
npm install drizzle-kit --save-dev

عيّن سلسلة اتصال قاعدة البيانات في .env:

# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

تكوين drizzle-kit​

أنشئ ملف drizzle.config.ts في جذر مشروعك:

// 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!,
},
verbose: true,
strict: true,
});
warning

إذا كنت تستخدم MySQL، عيّن dialect: 'mysql' في التكوين. إذا كنت تستخدم SQLite، عيّن dialect: 'sqlite'. يحدد حقل dialect أي نكهة SQL يُنشئها drizzle-kit للتهجرات. اللهجة غير الصحيحة ستُنتج ملفات تهجيرة تفشل على قاعدة بياناتك.

إنشاء عميل قاعدة البيانات​

// src/db/client.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';

const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
});

export const db = drizzle(pool, { schema });
export type DB = typeof db;
تلميح

استورد دائماً كائن المخطط ومرره إلى مُنشئ drizzle(). هذا يُفعّل واجهة الاستعلام العلائقية (db.query.users.findMany()) التي توفر جلب بيانات متداخل مع أمان أنواع كامل.

تعريف المخطط​

تُعرّف مخططات Drizzle في ملفات TypeScript باستخدام دوال البناء. كل دالة ترتبط بنوع عمود SQL.

تعريف الجداول​

// src/db/schema.ts
import {
pgTable,
uuid,
varchar,
text,
boolean,
integer,
timestamp,
enum,
pgEnum,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

// Enums
export const userRoleEnum = pgEnum('user_role', ['admin', 'member', 'viewer']);

// Users table
export const users = pgTable('users', {
id: uuid('id').defaultRandom().primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }),
role: userRoleEnum('role').default('member').notNull(),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

// Posts table
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
viewCount: integer('view_count').default(0).notNull(),
authorId: uuid('author_id').references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

// Profiles table
export const profiles = pgTable('profiles', {
id: uuid('id').defaultRandom().primaryKey(),
bio: text('bio'),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});

أنواع الأعمدة الشائعة (PostgreSQL)​

دالة Drizzleنوع SQLملاحظات
uuid()UUIDاستخدم .defaultRandom() للإنشاء التلقائي
varchar({ length: n })VARCHAR(n)سلسلة متغيرة الطول
text()TEXTسلسلة بطول غير محدود
integer()INTEGERعدد صحيح 32-بت
boolean()BOOLEANصحيح/خطأ
timestamp()TIMESTAMPاستخدم .defaultNow() للتعبئة التلقائية
jsonb()JSONBبيانات منظمة
serial()SERIALعدد صحيح يزداد تلقائياً
pgEnum()ENUMنوع enum في PostgreSQL
decimal()DECIMALعدد نقطي ثابت
تلميح

اطلب من Cursor: "Generate a Drizzle schema for PostgreSQL with User, Post, Comment, and Tag models. Use UUID primary keys, include timestamps, and set up one-to-many and many-to-many relations." سينتج Cursor مخطط TypeScript مُتحققاً منه مع استيرادات وتعريفات علاقات صحيحة.

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

تُعرّف علاقات Drizzle بشكل منفصل عن الجداول باستخدام دالة relations(). إنها لا تؤثر على مخطط قاعدة البيانات -- إنها تُكوّن فقط واجهة الاستعلام العلائقية:

// src/db/schema.ts (continued)
import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ one, many }) => ({
profile: one(profiles, {
fields: [users.id],
references: [profiles.userId],
}),
posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
}));

export const profilesRelations = relations(profiles, ({ one }) => ({
user: one(users, {
fields: [profiles.userId],
references: [users.id],
}),
}));

export const comments = pgTable('comments', {
id: uuid('id').defaultRandom().primaryKey(),
content: text('content').notNull(),
authorId: uuid('author_id').references(() => users.id, { onDelete: 'cascade' }),
postId: uuid('post_id').references(() => posts.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const commentsRelations = relations(comments, ({ one }) => ({
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
}));
warning

علاقات Drizzle مخصصة لواجهة الاستعلام فقط ولا تُنشئ قيود مفاتيح أجنبية في قاعدة البيانات. المفتاح الأجنبي الفعلي يُعرّف في تعريف الجدول باستخدام .references(). إذا عرّفت علاقة في relations() ولكن نسيت .references() في الجدول، سيعمل الاستعلام العلائقي لكن قاعدة البيانات لن تُطبّق سلامة الإحالة.

التهجرات مع drizzle-kit​

يُنشئ drizzle-kit ملفات تهجيرة SQL بمقارنة تعريف مخططك مع الحالة الحالية لقاعدة البيانات.

إنشاء التهجرات​

npx drizzle-kit generate

هذا يُنشئ ملف تهجيرة SQL في دليل drizzle/ (كما هو مُكوّن في drizzle.config.ts):

-- drizzle/0000_initial_migration.sql
CREATE TYPE "user_role" AS ENUM('admin', 'member', 'viewer');

CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(255) NOT NULL,
"name" varchar(100),
"role" "user_role" DEFAULT 'member' NOT NULL,
"avatar_url" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);

CREATE TABLE "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar(200) NOT NULL,
"content" text,
"published" boolean DEFAULT false NOT NULL,
"view_count" integer DEFAULT 0 NOT NULL,
"author_id" uuid,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);

ALTER TABLE "posts" ADD COLUMN "author_id" uuid REFERENCES "users"("id") ON DELETE CASCADE;

تطبيق التهجرات​

npx drizzle-kit migrate

هذا يُطبّق جميع ملفات التهجيرة المعلقة على قاعدة بياناتك بالترتيب.

تلميح

استخدم npx drizzle-kit generate بعد كل تغيير مخطط، ثم راجع SQL المُنشأ قبل تشغيل migrate. إنشاء التهجرات في Drizzle ممتاز لكنه ليس مثالياً -- تحقق دائماً من أن SQL يطابق نيتك، خاصة للعمليات المعقدة مثل إعادة تسمية الأعمدة أو تغيير الأنواع.

دفع تغييرات المخطط مباشرة​

للنماذج الأولية السريعة، يمكنك دفع تغييرات المخطط مباشرة إلى قاعدة البيانات دون إنشاء ملفات تهجيرة:

npx drizzle-kit push

هذا يُزامن مخططك مع قاعدة البيانات فوراً.

warning

يتخطى drizzle-kit push ملفات التهجرات ويُطبّق التغييرات مباشرة. هذا ممتاز للتطوير لكن يجب ألا يُستخدم أبداً في الإنتاج. استخدم دائماً generate + migrate لعمليات نشر الإنتاج للحفاظ على سجل تهجيرة مُنمذج.

سجل التهجرات​

يحافظ drizzle-kit على ملف meta/_journal.json يتتبع التهجرات المُطبقة:

[
{
"version": "5",
"idx": 0,
"when": 1700000000000,
"tag": "initial_migration",
"breakpoints": true
}
]

الاستعلامات الآمنة الأنواع​

منشئ استعلامات Drizzle مُحدد الأنواع بالكامل. كل استعلام تكتبه يُفحص في وقت التجميع، و IntelliSense في Cursor يوفر إكمالاً تلقائياً لأسماء الجداول والأعمدة والعوامل.

استعلامات التحديد​

import { eq, and, gte, lte, desc, asc, sql } from 'drizzle-orm';
import { db } from './client';
import { users, posts, profiles } from './schema';

// Select specific columns
const userEmails = await db
.select({ id: users.id, email: users.email, name: users.name })
.from(users);

// Select with filtering
const publishedPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);

// Select with multiple conditions
const recentPosts = await db
.select()
.from(posts)
.where(
and(
eq(posts.published, true),
gte(posts.createdAt, new Date('2024-01-01')),
lte(posts.createdAt, new Date('2024-12-31'))
)
)
.orderBy(desc(posts.viewCount), asc(posts.createdAt));

الوصلات​

// Inner join: posts with their authors
const postsWithAuthors = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true));

// Left join: users with optional profiles
const usersWithProfiles = await db
.select({
userId: users.id,
email: users.email,
bio: profiles.bio,
})
.from(users)
.leftJoin(profiles, eq(users.id, profiles.userId));

استعلامات الإدراج​

// Insert a single record
const [newUser] = await db
.insert(users)
.values({
name: 'New User',
role: 'member',
})
.returning();

// Insert multiple records
const newPosts = await db
.insert(posts)
.values([
{ title: 'Post 1', content: 'Content 1', authorId: userId },
{ title: 'Post 2', content: 'Content 2', authorId: userId },
])
.returning({ id: posts.id, title: posts.title });

// Insert with conflict handling (upsert)
const [upsertedUser] = await db
.insert(users)
.values({ email: '[email protected]', name: 'Updated Name' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Updated Name', updatedAt: new Date() },
})
.returning();
تلميح

اطلب من Cursor: "Write a Drizzle upsert query that inserts a new user if the email does not exist, or updates the name and avatarUrl if it does. Return the inserted/updated record." سيقوم Cursor بإنشاء صياغة onConflictDoUpdate بشكل صحيح.

التحديث والحذف​

// Update records
const [updatedPost] = await db
.update(posts)
.set({ title: 'Updated Title', viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, postId))
.returning();

// Delete records
await db.delete(posts).where(eq(posts.authorId, deletedUserId));

// Delete with condition
const deletedCount = await db
.delete(posts)
.where(and(eq(posts.published, false), lt(posts.createdAt, thirtyDaysAgo)))
.returning();

واجهة الاستعلام العلائقية​

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

// Fetch a user with their profile and posts
const userWithData = await db.query.users.findFirst({
where: eq(users.email, '[email protected]'),
with: {
profile: true,
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 5,
with: {
comments: {
with: {
author: {
columns: { id: true, name: true, avatarUrl: true },
},
},
},
},
},
},
});

// Fetch all published posts with author and comments
const allPosts = await db.query.posts.findMany({
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 20,
with: {
author: {
columns: { id: true, name: true, email: true },
},
comments: {
orderBy: desc(comments.createdAt),
with: {
author: true,
},
},
},
});
warning

تتطلب واجهة الاستعلام العلائقية (db.query.*) أن تمرر مخططك إلى مُنشئ drizzle(): drizzle(pool, { schema }). بدون المخطط، ستكون مساحة اسم db.query غير مُعرّفة وجميع الاستعلامات العلائقية ستُلقي خطأ وقت تشغيل.

التجميعات و Group By​

import { count, sql } from 'drizzle-orm';

// Count posts per author
const postCounts = await db
.select({
authorId: posts.authorId,
authorName: users.name,
postCount: count(posts.id),
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.groupBy(posts.authorId, users.name)
.orderBy(desc(count(posts.id)))
.limit(5);

// Sum and average
const stats = await db
.select({
totalViews: sql<number>`sum(${posts.viewCount})`,
avgViews: sql<number>`avg(${posts.viewCount})`,
postCount: count(posts.id),
})
.from(posts)
.where(eq(posts.published, true));

المعاملات​

يدعم Drizzle كلاً من المعاملات القائمة على الاستدعاء والقائمة على العبارات:

// Callback-based transaction
const result = await db.transaction(async (tx) => {
const [newUser] = await tx
.insert(users)
.values({ email: '[email protected]', name: 'Author' })
.returning();

const [newPost] = await tx
.insert(posts)
.values({
title: 'First Post',
content: 'Hello',
authorId: newUser.id,
published: true,
})
.returning();

await tx
.insert(profiles)
.values({ bio: 'Author bio', userId: newUser.id })
.returning();

return { user: newUser, post: newPost };
});
تلميح

اطلب من Cursor: "Write a Drizzle transaction that creates a user, assigns them to a workspace as a member, and creates a default project. Roll back on any error." سيقوم Cursor بإنشاء المعاملة القائمة على الاستدعاء مع انتشار خطأ مناسب.

بناء الاستعلامات بمساعدة الذكاء الاصطناعي في Cursor​

يمكن لمساعد الذكاء الاصطناعي في Cursor إنشاء استعلامات Drizzle معقدة وتعريفات مخطط واستراتيجيات تهجيرة من أوصاف اللغة الطبيعية.

إنشاء الاستعلامات من الأوامر​

افتح Cursor Chat (Ctrl+L) وصف ما تحتاجه:

Write a Drizzle ORM query to fetch the top 10 most-viewed published posts
from the last 30 days, including each post's author name and email, the
count of comments, and the count of likes. Order by view count descending.
Use the relational query API (db.query.posts.findMany).
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const topPosts = await db.query.posts.findMany({
where: and(
eq(posts.published, true),
gte(posts.createdAt, thirtyDaysAgo)
),
orderBy: desc(posts.viewCount),
limit: 10,
with: {
author: {
columns: { id: true, name: true, email: true },
},
},
extras: {
commentCount: sql<number>`(
SELECT count(*) FROM comments WHERE comments.post_id = posts.id
)`.as('commentCount'),
likeCount: sql<number>`(
SELECT count(*) FROM likes WHERE likes.post_id = posts.id
)`.as('likeCount'),
},
});
تلميح

عند طلب إنشاء استعلامات Drizzle من Cursor، حدد ما إذا كنت تريد واجهة منشئ الاستعلامات (db.select().from()) أم واجهة الاستعلام العلائقية (db.query.*.findMany()). للواجهتين صياغة ومقايضات مختلفة -- يمنحك منشئ الاستعلامات تحكماً أكبر في SQL بينما الواجهة العلائقية أكثر تعريفية.

إنشاء المخططات من الأوصاف​

Generate a Drizzle schema for a multi-tenant SaaS application using
PostgreSQL. Include: Tenant, User (with tenantId), Subscription (with
plan enum: FREE/PRO/ENTERPRISE), ApiKey, and Webhook table. Use UUIDs,
timestamps, proper foreign keys with cascade delete, and add indexes on
tenantId columns.

تهجيرة المخطط بمساعدة الذكاء الاصطناعي​

عند إعادة هيكلة مخططك، اطلب المساعدة من Cursor:

I need to add a "soft delete" feature to my Drizzle schema. Add a
"deleted_at" timestamp column to the posts and comments tables that is
nullable. Then update my existing queries to filter out soft-deleted
records by default. Show me how to generate the migration.

سيقوم Cursor بـ:

  1. إضافة عمود deletedAt إلى كلا الجدولين.
  2. تحديث استعلامات التحديد لإضافة مرشحات isNull(posts.deletedAt).
  3. إنشاء أمر تهجيرة drizzle-kit.
warning

عند إضافة أعمدة الحذف الناعم، الاستعلامات الموجودة التي لا تُرشح بـ deletedAt ستُرجع السجلات المحذوفة ناعماً. اطلب من Cursor تدقيق جميع استعلاماتك بعد تغيير المخطط لضمان عدم تسريب أي بيانات محذوفة ناعماً.

الأداء وأفضل الممارسات​

استراتيجية الفهرسة​

import { index } from 'drizzle-orm/pg-core';

export const posts = pgTable(
'posts',
{
id: uuid('id').defaultRandom().primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
authorId: uuid('author_id').references(() => users.id),
published: boolean('published').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
authorIdx: index('posts_author_id_idx').on(table.authorId),
publishedIdx: index('posts_published_created_idx').on(
table.published,
table.createdAt
),
})
);

تجمع الاتصالات​

import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';

const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});

export const db = drizzle(pool, { schema, logger: true });
warning

في البيئات بلا خادم (Vercel، AWS Lambda)، استخدم مجمع اتصالات مثل PgBouncer أو مشغل بلا خادم مثل @neondatabase/serverless. اتصالات PostgreSQL المباشرة لا تتوسع جيداً في البيئات بلا خادم لأن كل مثيل دالة يُنشئ تجمعه الخاص، مما يستنزف حد اتصال قاعدة البيانات.

العبارات المُعدة مسبقاً​

import { sql } from 'drizzle-orm';

const findPostsByAuthor = db
.select()
.from(posts)
.where(and(eq(posts.authorId, sql.placeholder('authorId')), eq(posts.published, true)))
.prepare('findPostsByAuthor');

const result = await findPostsByAuthor.execute({ authorId: userId });

الخلاصة​

يُنشئ اقتران Drizzle ORM مع Cursor تجربة تطوير قواعد بيانات قوية وآمنة الأنواع. تعريف المخطط الأصلي لـ TypeScript يمنحك تحكماً كاملاً في SQL بينما يلتقط خادم لغة Cursor أخطاء الأنواع في الوقت الفعلي. يوفر drizzle-kit سير عمل تهجيرة موثوق بأمرين generate و migrate للإنتاج، و push للنماذج الأولية السريعة. واجهة الاستعلام المزدوجة -- بأسلوب المنشئ db.select().from() والتعريفية db.query.*.findMany() -- تتيح لك اختيار مستوى التجريد المناسب لكل استعلام. يسرّع مساعد الذكاء الاصطناعي في Cursor كل خطوة، من إنشاء مخططات كاملة لمجالات معقدة إلى كتابة استعلامات مُحسّنة مع وصلات وتجميعات ومعاملات. ابدأ بتعريف مخططك في TypeScript، كوّن drizzle-kit، استخدم generate و migrate لتطور المخطط، ودع Cursor يبني الاستعلامات الآمنة الأنواع لتركز على منطق تطبيقك.