طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 13 · الوحدة 5: تكامل قواعد البيانات مع ORM

ORM و Sequelize: النماذج والجداول

قاعدة بيانات حقيقية بلا كتابة SQL يدويًّا ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح فكرة ORM وما يقابل الجدول والصف والاستعلام.
  • تعرّف نماذج Sequelize بأنواع البيانات والقيود.
  • تنفّذ عمليات CRUD مع where و Op والفرز.
  • تعالج أخطاء التحقق والتكرار برموز حالة مناسبة.

١من المصفوفة إلى قاعدة البيانات

حتى الآن خزّنا البيانات في مصفوفة داخل الخادم، فتضيع مع كل إعادة تشغيل. البيانات الحقيقية تُحفظ في قاعدة بيانات علائقية كالتي درستها (جداول وصفوف و SQL). لكن كتابة SQL نصًّا داخل JavaScript مرهقة وعرضة للأخطاء والحقن.

ORM (ربط الكائنات بالعلاقات Object-Relational Mapping) مكتبة تترجم بين العالمين: الجدول يصبح نموذجًا Model، والصف يصبح كائنًا، والاستعلام يصبح استدعاء دالة. وأشهرها في Node: Sequelize.

ما تريدهSQLSequelize
كل الكتبSELECT * FROM Books;Book.findAll()
كتاب برقمهSELECT * FROM Books WHERE id = 2;Book.findByPk(2)
إضافةINSERT INTO Books (title) VALUES ('…');Book.create({ title: '…' })
تعديلUPDATE Books SET year = 2024 WHERE id = 2;book.update({ year: 2024 })
حذفDELETE FROM Books WHERE id = 2;Book.destroy({ where: { id: 2 } })
🖥 التثبيت على جهازك
$ npm install sequelize sqlite3added 34 packages in 6s

Sequelize يعمل مع SQLite و MySQL و PostgreSQL وغيرها؛ تغيير قاعدة البيانات يعني تغيير سطر الاتصال فقط، والكود نفسه يبقى.

٢الاتصال والنموذج

نفصل الاتصال في ملف db.js، وكل نموذج في ملف داخل models/. sequelize.define يصف الجدول: اسم كل عمود ونوعه وقيوده. ويضيف Sequelize وحده العمود id مفتاحًا أساسيًّا، والعمودين createdAt و updatedAt.

DataTypesفي SQLمثال
STRINGVARCHAR(255)الاسم، البريد
TEXTTEXTوصف طويل
INTEGERINTEGERالسنة، الكمية
FLOAT / DECIMALFLOATالسعر
BOOLEANTINYINT(1)متاح؟
DATEDATETIMEتاريخ الإعارة

و sequelize.sync() ينشئ الجداول من النماذج إن لم تكن موجودة. لاحظ في الطرفية جملة SQL التي ولّدها Sequelize، فقد جعلنا logging هنا console.log:

const express = require('express');const sequelize = require('./db');const Book = require('./models/Book'); const app = express();app.use(express.json()); async function start() {  await sequelize.sync();                      // أنشئ الجداول إن لم توجد  app.listen(3000, () => console.log('المكتبة جاهزة'));}start();

الطلب إلى / رد بـ 404 لأننا لم نعرّف مسارات بعد؛ المهم هنا ما في الطرفية: جملة CREATE TABLE كاملة بالأنواع والقيود.

٣العمليات الأربع CRUD

كل دوال Sequelize غير متزامنة (تعيد Promise لأن قاعدة البيانات تأخذ وقتًا)، فنستخدم async و await في المعالجات:

const express = require('express');const sequelize = require('./db');const Book = require('./models/Book'); const app = express();app.use(express.json()); app.get('/api/books', async (req, res) => {  const books = await Book.findAll({ order: [['year', 'DESC']] });  res.json(books);}); app.get('/api/books/:id', async (req, res) => {  const book = await Book.findByPk(req.params.id);  if (!book) return res.status(404).json({ error: 'غير موجود' });  res.json(book);}); app.post('/api/books', async (req, res) => {  try {    const book = await Book.create(req.body);    res.status(201).json(book);  } catch (err) {    res.status(400).json({ error: err.errors.map(e => e.message) });  }}); app.patch('/api/books/:id', async (req, res) => {  const book = await Book.findByPk(req.params.id);  if (!book) return res.status(404).json({ error: 'غير موجود' });  await book.update(req.body);  res.json(book);}); app.delete('/api/books/:id', async (req, res) => {  const n = await Book.destroy({ where: { id: req.params.id } });  if (!n) return res.status(404).json({ error: 'غير موجود' });  res.sendStatus(204);}); async function start() {  await sequelize.sync();                      // أنشئ الجداول إن لم توجد  await Book.bulkCreate([    { title: 'تعلم JavaScript', author: 'سارة', year: 2021 },    { title: 'أساسيات الويب', author: 'خالد', year: 2023 },    { title: 'قواعد البيانات', author: 'سارة', year: 2019, available: false },  ]);  app.listen(3000, () => console.log('المكتبة جاهزة'));}start();

كل كتاب يعود ومعه id و createdAt و updatedAt. والطلب الرابع رُفض برسالتين: المؤلف مطلوب (allowNull: false)، والسنة أقل من 1900 (validate). هذا التحقق تم في النموذج قبل أن تصل البيانات لقاعدة البيانات.

٤البحث والفرز والترقيم

الخيار where يقابل WHERE في SQL، ومعه المعاملات Op للمقارنات. ونحدد الأعمدة بـ attributes، والفرز بـ order، والترقيم بـ limit و offset:

const express = require('express');const sequelize = require('./db');const Book = require('./models/Book'); const app = express();app.use(express.json());const { Op } = require('sequelize'); // /api/books?author=سارة&from=2020app.get('/api/books', async (req, res) => {  const where = {};  if (req.query.author) where.author = req.query.author;  if (req.query.from) where.year = { [Op.gte]: Number(req.query.from) };  const books = await Book.findAll({    where,    attributes: ['id', 'title', 'year'],    order: [['year', 'ASC']],    limit: 10,  });  res.json(books);}); app.get('/api/stats', async (req, res) => {  res.json({    total: await Book.count(),    available: await Book.count({ where: { available: true } }),  });}); async function start() {  await sequelize.sync();                      // أنشئ الجداول إن لم توجد  await Book.bulkCreate([    { title: 'تعلم JavaScript', author: 'سارة', year: 2021 },    { title: 'أساسيات الويب', author: 'خالد', year: 2023 },    { title: 'قواعد البيانات', author: 'سارة', year: 2019, available: false },  ]);  app.listen(3000, () => console.log('المكتبة جاهزة'));}start();
OpSQLمثال
Op.gt / Op.gte> / >={ price: { [Op.gt]: 100 } }
Op.lt / Op.lte< / <={ year: { [Op.lt]: 2000 } }
Op.ne!={ status: { [Op.ne]: 'ملغى' } }
Op.inIN (…){ id: { [Op.in]: [1, 2, 5] } }
Op.likeLIKE{ title: { [Op.like]: '%ويب%' } }

٥القيود والأخطاء

القيد unique تفرضه قاعدة البيانات نفسها، فيظهر خطأ من نوع SequelizeUniqueConstraintError؛ أما validate و allowNull فيفحصهما Sequelize قبل الحفظ (SequelizeValidationError). نميّز بينهما باسم الخطأ err.name:

const express = require('express');const { Sequelize, DataTypes } = require('sequelize');const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }); const User = sequelize.define('User', {  email: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { isEmail: true } },  name:  { type: DataTypes.STRING, allowNull: false, validate: { len: [2, 40] } },}); const app = express();app.use(express.json()); app.post('/api/users', async (req, res) => {  try {    const u = await User.create(req.body);    res.status(201).json({ id: u.id, email: u.email });  } catch (err) {    if (err.name === 'SequelizeUniqueConstraintError') return res.status(409).json({ error: 'البريد مسجّل مسبقًا' });    if (err.name === 'SequelizeValidationError') return res.status(400).json({ error: err.errors.map(e => e.message) });    throw err;  }}); sequelize.sync().then(() => app.listen(3000));

409 Conflict مناسب للتكرار، و400 للبيانات غير الصالحة. ورسائل validate جاهزة بالإنجليزية؛ يمكنك تخصيصها: validate: { isEmail: { msg: 'بريد غير صالح' } }.

توقّع وتحقّق

ما الذي يقابل الجدول في Sequelize؟

define.

أي دالة تجلب صفًّا بمفتاحه الأساسي؟

Primary Key.

ما وظيفة sequelize.sync()؟

مزامنة البنية.

allowNull: false تعني:

NOT NULL.

الخلاصة

تحدٍّ برمجي: مخزن المنتجات

متوسط

خادم منتجات يحفظ بياناته في قاعدة بيانات عبر Sequelize بدل المصفوفة.

المطلوب:
  • نموذج Product فيه name (نص، مطلوب) و price (عدد عشري، مطلوب، لا يقل عن 0) و stock (عدد صحيح، افتراضيًّا 0).
  • POST /api/products ينشئ منتجًا ويرد بـ 201، أو بـ 400 إن فشل التحقق.
  • GET /api/products?max=100 يرد بالمنتجات التي سعرها أقل من أو يساوي max، مرتبة بالسعر تصاعديًّا (وبلا max: كلها).
  • DELETE /api/products/:id يحذف ويرد بـ 204، أو 404 إن لم يوجد.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /api/products {"name": "شاشة", "price": 899.5, "stock": 4}
  2. POST /api/products {"name": "كابل", "price": 25}
  3. POST /api/products {"name": "فأرة", "price": 60}
  4. POST /api/products {"name": "سعر سالب", "price": -5}
  5. POST /api/products {"price": 10}
  6. GET /api/products?max=100
  7. DELETE /api/products/2
  8. DELETE /api/products/2
  9. GET /api/products
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • إنشاء منتج ← 201
  • … والرد فيه المنتج و stock = 4
  • stock افتراضيًّا 0
  • السعر السالب ← 400
  • بلا اسم ← 400
  • max=100 يعيد الكابل ثم الفأرة فقط
  • … منتجين بالضبط
  • الحذف ← 204
  • حذف غير الموجود ← 404
  • بقي منتجان

💡 تلميح 1
validate: { min: 0 } على السعر، و defaultValue: 0 على المخزون.
💡 تلميح 2
destroy يعيد عدد الصفوف المحذوفة: إن كان 0 فالمنتج غير موجود.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const { Sequelize, DataTypes, Op } = require('sequelize');const sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }); const Product = sequelize.define('Product', {  name:  { type: DataTypes.STRING, allowNull: false },  price: { type: DataTypes.FLOAT, allowNull: false, validate: { min: 0 } },  stock: { type: DataTypes.INTEGER, defaultValue: 0 },}); const app = express();app.use(express.json()); app.post('/api/products', async (req, res) => {  try {    res.status(201).json(await Product.create(req.body));  } catch (err) {    res.status(400).json({ error: err.message });  }}); app.get('/api/products', async (req, res) => {  const where = req.query.max ? { price: { [Op.lte]: Number(req.query.max) } } : {};  res.json(await Product.findAll({ where, order: [['price', 'ASC']] }));}); app.delete('/api/products/:id', async (req, res) => {  const n = await Product.destroy({ where: { id: req.params.id } });  if (!n) return res.status(404).json({ error: 'غير موجود' });  res.sendStatus(204);}); sequelize.sync().then(() => app.listen(3000));
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس