SELFLAB الستاك الكامل · ١١ إقليم SYSTEM ONLINE
الإقليم 04منهجٌ يُدرَس وحدك

الإقليم ٤ — النموذج العلائقيّ و SQL من الجذور

يبني فوق: "الـ database تحفظ الحالة" من منهج بنية الويب (صندوقٌ أسود نفتحه الآن)، وانعدامُ الحالة وقيدُ التفرّد من الإقليم ٠، وبنياتُ البيانات من C. يفتح: Prisma (٥)، والمعاملاتُ في التصحيح (١٠).

النبذة

في منهج بنية الويب تتبّعتَ الطلب حتى باب قاعدة البيانات، وقلنا "تحفظ الحالة"، وأقفلنا الصندوق. الآن نفتحه — لا من واجهة أداةٍ، بل من جذره الرياضيّ. قاعدةُ البيانات العلائقيّة ليست "مكاناً نرمي فيه البيانات"؛ إنها بناءٌ هندسيٌّ وُلد من نظرية المجموعات ليحلّ مشكلةً محدّدةً في حفظ الحقائق. من يفهم لماذا جداولٌ ومفاتيحُ وقيود، يصمّم بياناتٍ لا تفسد؛ ومن يحفظ أوامرَ SQL بلا هذا الجذر، يبني على رمل. وحين تأتي Prisma في الإقليم التالي، ستكون مجرّد راحةٍ فوق ما تتقنه، لا صندوقاً ثالثاً.


اللغز المستفزّ

مطلوبٌ منك حفظُ بيانات المنصّة: مستخدمون، مناهج، سبرنتات، مشاريع، مهامّ، حالاتُ اختبار، تسجيلاتٌ في المناهج، وتقدّمُ كلِّ متدرّبٍ في كل مهمّة. عقليّةُ المبرمج المبتدئ تقفز لأبسط حلّ: جدولٌ واحدٌ ضخمٌ مسطّح (أو ملفُّ JSON واحد) فيه كلُّ شيءٍ معاً، صفٌّ لكل "حدث":

user_namecurriculum_titlesprint_nametask_titletask_progress
yazeedأساسيات Cالأسبوع ١المؤشّرات80
yazeedأساسيات Cالأسبوع ١المصفوفات100
saraأساسيات Cالأسبوع ١المؤشّرات50
...............

اشتغل! حتى تصطدم بالواقع. توقّع كلَّ كارثةٍ قبل أن تقرأ حلّها:

  1. التكرار: عنوان "أساسيات C" مكرّرٌ في كل صفٍّ يخصّه — آلافُ النسخ لحقيقةٍ واحدة. ضياعُ مساحةٍ، نعم، لكن الأخطر آتٍ.
  2. شذوذُ التحديث (update anomaly): قرّرتَ تسمية المنهج "مدخل إلى C". الآن لازم تحدّث كل الصفوف. نسيتَ صفّاً واحداً؟ صار عندك منهجٌ باسمين متناقضين، ولا أحد يعرف الصحيح. الحقيقةُ الواحدةُ المبعثرةُ تتعفّن.
  3. شذوذُ الإدراج (insertion anomaly): أردتَ إضافةَ منهجٍ جديدٍ بلا مهامَّ بعد. لكن كلَّ صفٍّ يتطلّب task_title! فلا تستطيع تمثيلَ "منهجٌ موجودٌ بلا مهامّ" إلا بحقولٍ فارغةٍ كاذبة.
  4. شذوذُ الحذف (deletion anomaly): حذفتَ آخر مهمّةٍ في منهج. اختفى المنهجُ كلّه من الوجود — لأن وجوده كان طفيلياً على صفوف المهامّ.

السؤال المستفزّ:

ملاحظة

صمّم حفظاً تعيش فيه كلُّ حقيقةٍ في مكانٍ واحدٍ بالضبط (فلا شذوذ)، ومع ذلك تستطيع أن تجيب أسئلةً تعبر الحقائق ("ما سبرنتات هذا المنهج؟"، "ما المناهج التي سجّل فيها هذا المستخدم؟"). كيف تفصل الحقائق دون أن تفقد الروابط بينها؟

هذا التوتّر — فصلٌ بلا فقدان — هو ما حلّه النموذجُ العلائقيّ. اقعد معه.


ليش: لماذا "علائقيّ"؟ الجذر الرياضيّ

سنة ١٩٧٠، لاحظ إدغار كود أن خلطَ "ما البيانات" بـ"كيف تُخزَّن في الملفّات" يولّد هشاشةً (كلُّ ما رأيتَه أعلاه). فاقترح فصلاً جذرياً: نمذِج البياناتِ كـعلاقاتٍ رياضية (relations)، واترك "كيف تُخزَّن" للمحرّك. العلاقة = مجموعةٌ من الصفوف (tuples)، وكلُّ صفٍّ مجموعةٌ مرتّبةٌ من القيم، كلُّ قيمةٍ من مجالٍ (نوع). وهذا بالضبط ما يصير جدولاً:

code
جدول = علاقة = مجموعةُ صفوفٍ متجانسة صفّ = tuple = حقيقةٌ واحدة عمود = attribute بمجالٍ (نوع: text, integer, uuid, timestamp, boolean, enum)
نموذج ذهني

النموذج الذهني الجذر: الجدول ليس "ملفّاً بصفوف". إنه مجموعةٌ رياضية — لا ترتيبَ مضمونٌ للصفوف، ولا تكرارَ صفٍّ مطابقٍ كاملاً (لكلٍّ مفتاحٌ يميّزه)، وكلُّ عمليّةٍ عليه (استعلام) تُنتِج علاقةً جديدة (مجموعةً أخرى). لهذا SQL تصريحيّةٌ (تقول ماذا تريد) لا أمريّةٌ كـ C (كيف تجلبه) — لأنها جبرُ مجموعاتٍ، والمحرّك يقرّر التنفيذ. (لمستَ "النوع = مجموعة" في منهج TS؛ هنا "الجدول = مجموعة" — العائلةُ الفكريّةُ نفسها.)

الفصلُ بلا فقدانٍ يتحقّق بثلاث لبنات: المفاتيح تُعرّف الحقائق، والمفاتيح الأجنبية تربطها، والقيود تحرس سلامتها. نبنيها لبنةً لبنة.


كيف: لبنات النموذج

المفتاح الأساسيّ (primary key) — هويّةُ الصفّ

كلُّ صفٍّ يحتاج هويّةً وحيدةً ثابتةً تميّزه عن كل غيره. خياران:

مشروعك اختار UUID (id String @id @default(uuid()) @db.Uuid) — قرارٌ واعٍ: معرّفاتٌ لا تُخمَّن ولا تكشف حجمَ النظام، تصلح لمنصّةٍ موزّعة. (هذا أحدُ القرارات التي صرتَ تفهم لماذا اتُّخذت، لا تحفظها.)

قيدُ التفرّد (unique) — يحلّ بذرة الإقليم ٠

تذكّر سؤال idempotency: كيف تمنع POST /register المكرّر من خلق مستخدمين مزدوجين؟ الجواب ليس في كود الخادم وحده (سباقُ نقرتين متزامنتين يتجاوز أيَّ فحصٍ في التطبيق) — بل في قيدٍ تفرّضه قاعدة البيانات نفسها:

sql
username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE

الآن مهما تسابقت نقرتان، قاعدةُ البيانات ترفض الثانية بخطأٍ ذرّيّ. هذا هو خطّ الدفاع الأخير والوحيد المضمون. درسٌ ستبنيه في الإقليم ٦: التطبيقُ يتحقّق للطفِ التجربة، لكن قاعدةَ البيانات تتحقّق للصحّة الحتميّة. (في schema.prisma: @unique على username وemail؛ وفي الـ migration: CREATE UNIQUE INDEX.)

NOT NULL، الافتراضيّات، والقيود — الجدولُ حارسٌ

sql
password_hash TEXT NOT NULL, -- لا يُسمح بـ NULL role "Role" NOT NULL DEFAULT 'USER',-- قيمةٌ افتراضية created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CHECK (memory_limit_mb > 0) -- قيدُ منطقٍ (مثالٌ من الـ sandbox)

كلُّ قيدٍ منها قاعدةُ سلامةٍ يفرضها المحرّك مهما كان مصدرُ الكتابة (تطبيقك، سكربت، يدويّ في psql). قاعدةُ البيانات هي الحارسُ الأخير للحقيقة — أبعدُ من المترجم (الذي يُمحى) وأبعدُ من Zod (الذي يحرس حافةَ HTTP فقط). ثلاثُ طبقاتِ حراسةٍ ستراها مكتملةً في الإقليم ٦.

الأنواع المُعدَّدة (enum)

sql
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');

مجالٌ مقيَّدٌ بقيمٍ محدّدة — مطابقٌ تماماً لاتّحاد القيم الحرفية في TS ("USER" | "ADMIN" من الإقليم ١). لاحظ في تاريخ مشروعك: الـ migration الأولى جعلت role نصّاً بافتراضِ 'trainee'، ثم الثانية حوّلته إلى enum('USER','ADMIN') — وهذا بالضبط نوعُ تطوّر السكيمة الذي يديره نظامُ الـ migrations (الإقليم ٥). (وفيه تعارضٌ مع وثائق المشروع التي تقول trainee/curriculum_designer/admin — قرارٌ تملكه الآن.)

المفتاح الأجنبيّ (foreign key) — هنا تُربَط الحقائق المفصولة

هذه اللبنةُ التي تحلّ "الفصل بلا فقدان". بدل تكرار عنوان المنهج في كل مهمّة، نفصل المنهجَ في جدوله، ونضع في المهمّة إشارةً لمفتاحه:

sql
CREATE TABLE curricula ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL -- الحقيقةُ هنا، مرّةً واحدة ); CREATE TABLE sprints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, curriculum_id UUID NOT NULL REFERENCES curricula(id) ON DELETE CASCADE -- ↑ مفتاحٌ أجنبيّ: يشير لصفٍّ في curricula );

curriculum_id يخزّن هويّةَ المنهج لا نسخةً من بياناته. هذا يحقّق علاقة واحد-لكثير (CURRICULUM ─< SPRINT): منهجٌ واحدٌ له سبرنتاتٌ كثيرة، كلٌّ تشير إليه. وعنوانُ المنهج يعيش في مكانٍ واحدٍ بالضبط — لا شذوذَ تحديثٍ ولا حذف.

السلامةُ المرجعيّة (referential integrity): المحرّك يرفض curriculum_id يشير لمنهجٍ غيرِ موجود، ويرفض حذفَ منهجٍ له سبرنتات (أو يحذفها معه حسب ON DELETE). لا يمكن أن توجد سبرنتٌ يتيمة. الرابطُ مضمونٌ رياضياً، لا بأملٍ من كود التطبيق.

هكذا تنبني سلسلةُ مشروعك كلّها بمفاتيحَ أجنبية: USER ─< CURRICULUM ─< SPRINT ─< PROJECT ─< TASK ─< TEST_CASE.

كثير-لكثير: جدولُ الوصل (junction)

USER ─< ENROLLMENT >─ CURRICULUM: مستخدمٌ يسجّل في مناهجَ كثيرة، ومنهجٌ فيه مستخدمون كثيرون. لا يمكن تمثيلُ هذا بمفتاحٍ أجنبيٍّ في أيٍّ من الطرفين (أين تضع "قائمةً"؟ العلائقيّ لا يحفظ قوائم في خلية). الحلُّ الوحيد: جدولٌ ثالثٌ للوصل، كلُّ صفٍّ فيه = رابطٌ واحد:

sql
CREATE TABLE enrollments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, curriculum_id UUID NOT NULL REFERENCES curricula(id) ON DELETE CASCADE, enrolled_at TIMESTAMP NOT NULL DEFAULT now(), UNIQUE (user_id, curriculum_id) -- لا تسجيلَ مزدوجٌ لنفس الزوج );

وبالمثل TASK_PROGRESS يصل ENROLLMENT بـ TASK (تقدّمُ تسجيلٍ معيّنٍ في مهمّةٍ معيّنة) — وهو حيث يكتب نظامُ التصحيح نتيجتَه في الإقليم ١٠.

التطبيع (normalization) — تسميةُ ما فعلناه

ما فعلناه للتوّ اسمه التطبيع: تفكيكُ البيانات حتى تعيش كلُّ حقيقةٍ مرّةً واحدة. القواعدُ الثلاثُ الأولى، مشتقّةً من الشذوذ لا محفوظة:

التطبيعُ ليس طقساً؛ هو العلاج المشتقُّ من الداء الذي رأيته في اللغز. (ويوجد إلغاءُ تطبيعٍ مقصودٌ أحياناً للأداء — مقايضةٌ واعية، بذرة.)


كيف: SQL — لغةُ الاستجواب التصريحيّة

SQL تختلف عن C جوهرياً: تصف النتيجة المطلوبة، لا خطواتِ الحصول عليها. تقول "أعطني صفوفَ المستخدمين حيث الدور admin مرتّبةً بالاسم"، ويقرّر مخطِّطُ الاستعلام (query planner) كيف (أيُّ فهرسٍ يمسح، أيُّ ترتيبِ ضمّ). هذا تجريدٌ قويّ — وأحياناً فخّ، حين لا تفهم ما يفعله المخطِّط (الفهارس أدناه).

DDL — تعريفُ البنية (رأيتَه أعلاه: CREATE TYPE/TABLE)

DML — التعامل مع البيانات

sql
-- إدراج INSERT INTO users (first_name, last_name, username, email, password_hash) VALUES ('Yazeed', 'A', 'yazeed', 'y@x.com', '$2b$...') RETURNING id, created_at; -- يرجّع ما ولّدته القاعدة -- استعلام: إسقاطٌ (أعمدة) + ترشيحٌ (WHERE) + ترتيبٌ + حدّ SELECT id, username, role FROM users WHERE role = 'ADMIN' AND created_at > now() - interval '7 days' ORDER BY created_at DESC LIMIT 20; -- تحديثٌ وحذفٌ مرشَّحان (انتبه: WHERE وإلّا طالت كلَّ الصفوف!) UPDATE users SET role = 'ADMIN' WHERE id = '...'; DELETE FROM users WHERE id = '...';

JOIN — قلبُ النموذج: نلملم ما فصلناه

التطبيعُ فصلَ الحقائقَ في جداول. الـ JOIN يعيد تركيبها عند السؤال — يضمّ صفوفاً من جدولين حيث يتطابق مفتاحٌ أجنبيٌّ مع مفتاحٍ أساسيّ. تذكّر تداخلَ الإقليم ٠ (/curricula/:id/sprints)؟ هو حرفياً هذا الضمّ:

sql
-- كل سبرنتات منهجٍ معيّن (الواحد-لكثير، معكوساً) SELECT s.id, s.name FROM sprints s JOIN curricula c ON s.curriculum_id = c.id WHERE c.id = '...'; -- كل المناهج التي سجّل فيها مستخدمٌ (عبور جدول الوصل — كثير لكثير) SELECT c.title, e.enrolled_at FROM enrollments e JOIN curricula c ON e.curriculum_id = c.id WHERE e.user_id = '...';

التجميع (aggregation)

sql
-- عدد المهامّ في كل مشروع SELECT project_id, COUNT(*) AS task_count FROM tasks GROUP BY project_id; -- عدد المسجّلين في كل منهج SELECT c.title, COUNT(e.id) AS learners FROM curricula c LEFT JOIN enrollments e ON e.curriculum_id = c.id GROUP BY c.id, c.title;

GROUP BY يطوي صفوفاً في مجموعاتٍ، ودوالُّ التجميع (COUNT/SUM/AVG/MAX) تلخّص كلَّ مجموعة. هذا كيف تحسب تقدّمَ متدرّبٍ أو إحصاءاتِ منهج.

المعاملات و ACID — لا غنى عنها للتصحيح (الإقليم ١٠)

تخيّل تسجيلَ نتيجةِ تصحيح: تكتب صفَّ نتيجةٍ، وتحدّث task_progress، وربما نقاطَ المتدرّب — ثلاثُ كتاباتٍ يجب أن تنجح كلُّها أو لا شيء. لو انهار الخادمُ بين الثانية والثالثة، تبقى البياناتُ نصفَ مكتوبةٍ متناقضة. الحلّ: المعاملة (transaction):

sql
BEGIN; INSERT INTO submissions (...) VALUES (...); UPDATE task_progress SET score = 80, status = 'passed' WHERE ...; COMMIT; -- إمّا الكلُّ يثبت ذرّياً... -- ROLLBACK; -- ...أو يُلغى الكلُّ كأنه لم يكن

ضماناتُ ACID:

هذه الضماناتُ هي لماذا نضع الحالةَ في قاعدةٍ علائقيّةٍ لا في ملفّ. وستبنيها بيدك في لغز التصحيح.

الفهارس (indexes) — لماذا يبطئ استعلامٌ ويسرع آخر

بلا فهرس، البحثُ عن WHERE email = '...' يمسح كلَّ الصفوف (sequential scan) — بطيءٌ مع الملايين. الفهرسُ بنيةُ بياناتٍ مساعدة (B-tree غالباً — تعرفها) تسمح بقفزٍ لوغاريتميٍّ للصفّ المطلوب:

sql
CREATE INDEX idx_sprints_curriculum ON sprints(curriculum_id); -- سرّع ضمّ المفاتيح الأجنبية EXPLAIN ANALYZE SELECT * FROM sprints WHERE curriculum_id = '...'; -- شاهد الخطّة

ملاحظتان: (١) قيودُ PRIMARY KEY وUNIQUE تُنشئ فهارسَها تلقائياً (لهذا رأيتَ CREATE UNIQUE INDEX users_email_key في migration مشروعك). (٢) المقايضة: الفهرسُ يسرّع القراءةَ لكن يبطئ الكتابةَ (يجب تحديثُه) ويأكل مساحة — ففهرِس أعمدةَ البحث والمفاتيحَ الأجنبية، لا كلَّ شيء. (EXPLAIN نافذتُك على قرارات المخطِّط — استعملها حين يبطئ استعلام.)


اللغز / البناء من الصفر

أدواتك: Postgres حقيقيّ وSQL خام بيدك — ممنوع أيُّ ORM. شغّله بحاوية (تعرف Docker): docker run --rm -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:16، ثم اتّصل: psql postgresql://postgres:dev@localhost:5432/postgres. (أو استعمل خدمة database في docker-compose.yml مشروعك.)

اللغز أ — اعِشِ الشذوذ ثم اقتله. أنشئ الجدولَ المسطّحَ الكارثيّ من اللغز المستفزّ (كلُّ شيءٍ في flat_data)، أدخِل صفوفاً تشترك في منهج. ثم: (١) غيّر عنوانَ المنهج بـ UPDATE وراقب كم صفٍّ تأثّر، واترك صفّاً واحداً قديماً عمداً — أنت الآن أمام شذوذ تحديث. (٢) حاول إدخالَ منهجٍ بلا مهمّة. (٣) احذف آخرَ مهمّةٍ في منهجٍ وشاهده يختفي. اكتب بجملةٍ لكلٍّ: أيُّ غيابٍ في التصميم سبّبه؟ ثم أعِد التصميم مطبَّعاً (الخطوة ب) وأثبِت أن الشذوذات اختفت.

اللغز ب — صمّم سكيمةَ المنصّة مطبّعةً، بـ DDL خام. أنشئ الجداول: users (بـ enum للدور، وusername/email فريدين، وuuid مفتاحاً)، curricula، sprints، projects، tasks، test_cases — مربوطةً بسلسلةِ مفاتيحَ أجنبية (USER ─< CURRICULUM ─< SPRINT ─< PROJECT ─< TASK ─< TEST_CASE)، وenrollments كجدولِ وصلٍ (مع UNIQUE(user_id, curriculum_id))، وtask_progress كجدولِ وصلٍ بين enrollments وtasks. القيود إلزاميّة: NOT NULL، DEFAULT، FK مع ON DELETE مناسب. هذا تصميمُ قاعدةِ بيانات مشروعك بيدك — حين تراها في Prisma لاحقاً ستكون ترجمةً لما صمّمتَه.

اللغز ج — أجِب أسئلةً تعبر الجداول (JOIN/GROUP BY). بعد إدخال بياناتِ تجربة، اكتب استعلاماتٍ تجيب: (١) كل سبرنتات منهجٍ معيّنٍ بالترتيب. (٢) كل المناهج التي سجّل فيها مستخدمٌ معيّن (عبر جدول الوصل). (٣) عددُ المهامّ في كل مشروع. (٤) كلُّ المناهج حتى ما لا متدرّبَ فيه مع عدد متدرّبيه (LEFT JOIN — لماذا LEFT لا INNER هنا؟). كلُّ سؤالٍ يلملم ما فصلتَه في (ب).

اللغز د — معاملةٌ ذرّيّة (بروفةُ التصحيح). بمعاملةٍ واحدة (BEGIN ... COMMIT)، سجّل "نتيجةَ تصحيح": أدخِل صفّاً في جدولِ نتائج، وحدّث task_progress المقابل. ثم أثبِت الذرّيّة: ابدأ معاملةً، نفّذ الكتابة الأولى، ثم تعمّد خطأً في الثانية (مثلاً اكتب لعمودٍ يخالف قيداً)، وROLLBACK، وتأكّد أن الأولى اختفت أيضاً — لا أثرَ لنصفِ كتابة. صف بكلماتك: لماذا هذه الذرّيّة لا غنى عنها حين يصحّح آلافُ المتدرّبين بالتوازي؟

اللغز هـ — الفهرس بعينك. على جدولٍ بآلاف الصفوف (ولّدها بـ generate_series)، نفّذ EXPLAIN ANALYZE لاستعلامِ بحثٍ بلا فهرس (لاحظ Seq Scan والزمن)، ثم CREATE INDEX، ثم أعِد EXPLAIN ANALYZE (لاحظ Index Scan والزمن). شاهدِ المخطِّطَ يغيّر خطّتَه. هذا فهمُك للأداء، لا حفظٌ.

ملاحظة

لا تنتقل قبل أن تبني سكيمةَ مشروعك بيدك (ب) وتراها تجيب أسئلةَ JOIN (ج) وتثبت ذرّيّةَ معاملةٍ (د). هذه الثلاثُ هي امتلاكُك لطبقة البيانات.


الخلاصة — أين تتّصل هذه العقدة بالشجرة

  • تحت: فتحنا "الـ database تحفظ الحالة" (بنية الويب) إلى جذرها: النموذج العلائقيّ — جداولُ كمجموعات، مفاتيحُ كهويّات، مفاتيحُ أجنبيةٌ كروابط، قيودٌ كحرّاس، تطبيعٌ يقتل الشذوذ. وSQL التصريحيّة لاستجوابها (JOIN لِلملمة، GROUP BY للتلخيص، المعاملات للذرّيّة، الفهارس للأداء).
  • العقدة الجديدة: حفظُ الحقائق بلا شذوذٍ مع روابطَ مضمونة — وهو نظامُ السجلّ الذي يحفظ حالةَ منصّتك بينما يبقى الخادمُ عديمَ الحالة (الإقليم ٠).
  • فوق: قيدُ التفرّد يحلّ idempotency التسجيل (٠)؛ القاعدةُ هي حارسُ الصحّة الأخير الذي يكمل طبقاتِ الحراسة (٦)؛ المعاملاتُ الذرّيّةُ هي كيف يكتب التصحيحُ نتيجتَه (١٠). والآن السؤال: هل نكتب كلَّ هذا SQL بأيدينا في الخادم؟

الأبواب المفتوحة: كتابةُ SQL خامٍ في كل متحكّمٍ مملّةٌ ومعرّضةٌ للحقن (injection)، ونتائجُه تأتي بلا نوعٍ (any — تذكّر erasure). كيف نربط جداولَنا بكائنات JS بأمانٍ ونوعٍ دون أن نخسر فهمَ SQL تحته؟ (٥).


الوصلة للواقع (نمط SelfLab)

افتح schema.prisma و migration مشروعك الآن — ستقرؤهما كترجمةٍ لِما صمّمتَه:

prisma
model User { id String @id @default(uuid()) @db.Uuid // مفتاحٌ أساسيٌّ UUID username String @unique @map("user_name") // قيدُ تفرّد email String @unique password String @map("password_hash") // NOT NULL ضمناً role Role @default(USER) // enum بقيمةٍ افتراضية ... @@map("users") // اسمُ الجدول الفعليّ }

و migration الـ SQL الذي ولّده يطابق ما كتبتَه بيدك: CREATE TABLE "users" (...), CREATE UNIQUE INDEX "users_email_key", CREATE TYPE "Role" AS ENUM (...). لم يعد شيءٌ منه غامضاً: تعرف لماذا UUID، لماذا unique، لماذا enum، ومن أين تأتي الفهارس. وتعرف ما ينقص سكيمةَ مشروعك (بقيّةُ الكيانات: curricula → sprints → ... → test_cases، وenrollments، وtask_progress) — وصرتَ قادراً على إضافتها.

بذرة الإقليم التالي

البذرة التالية: صمّمتَ الجداولَ وكتبتَ SQL بيدك — فلماذا في كود مشروعك لا ترى INSERT INTO users بل prisma.user.create({ data: {...} })؟ ومن أين أتى newUser بنوعٍ كاملٍ (يعرف حقولَه) مع أن الأنواع تُمحى وقت التشغيل (الإقليم ١)؟ ولماذا طبقةٌ فوق SQL أصلاً إن كان SQL يكفي؟ ننزل للإقليم ٥ نفهم Prisma كـ ORM: ليش وُجد، وخطُّ schema→migrate→generate، وكيف يعيد لك النوعَ الذي محته اللغة.

الستاك الكامل · من باب الخادم إلى داخله ليش قبل كيف · صُمّم لـ يزيد