ORM و Sequelize: النماذج والجداول
قاعدة بيانات حقيقية بلا كتابة SQL يدويًّا ⏱ نحو 45 دقيقة.
- تشرح فكرة
ORMوما يقابل الجدول والصف والاستعلام. - تعرّف نماذج
Sequelizeبأنواع البيانات والقيود. - تنفّذ عمليات
CRUDمعwhereوOpوالفرز. - تعالج أخطاء التحقق والتكرار برموز حالة مناسبة.
١من المصفوفة إلى قاعدة البيانات
حتى الآن خزّنا البيانات في مصفوفة داخل الخادم، فتضيع مع كل إعادة تشغيل. البيانات الحقيقية تُحفظ في قاعدة بيانات علائقية كالتي درستها (جداول وصفوف و SQL). لكن كتابة SQL نصًّا داخل JavaScript مرهقة وعرضة للأخطاء والحقن.
ORM (ربط الكائنات بالعلاقات Object-Relational Mapping) مكتبة تترجم بين العالمين: الجدول يصبح نموذجًا Model، والصف يصبح كائنًا، والاستعلام يصبح استدعاء دالة. وأشهرها في Node: Sequelize.
| ما تريده | SQL | Sequelize |
|---|---|---|
| كل الكتب | 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 | مثال |
|---|---|---|
STRING | VARCHAR(255) | الاسم، البريد |
TEXT | TEXT | وصف طويل |
INTEGER | INTEGER | السنة، الكمية |
FLOAT / DECIMAL | FLOAT | السعر |
BOOLEAN | TINYINT(1) | متاح؟ |
DATE | DATETIME | تاريخ الإعارة |
و 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();
// db.js: الاتصال بقاعدة البيانات (SQLite في ملف؛ وفي الإنتاج MySQL أو PostgreSQL)const { Sequelize } = require('sequelize'); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'library.sqlite', logging: console.log, // اطبع كل جملة SQL}); module.exports = sequelize;
// models/Book.js: نموذج = جدولconst { DataTypes } = require('sequelize');const sequelize = require('../db'); const Book = sequelize.define('Book', { title: { type: DataTypes.STRING, allowNull: false }, author: { type: DataTypes.STRING, allowNull: false }, year: { type: DataTypes.INTEGER, validate: { min: 1900, max: 2100 } }, available: { type: DataTypes.BOOLEAN, defaultValue: true },}); module.exports = Book;
الطلب إلى / رد بـ 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();
// db.js: الاتصال بقاعدة البيانات (SQLite في ملف؛ وفي الإنتاج MySQL أو PostgreSQL)const { Sequelize } = require('sequelize'); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'library.sqlite', logging: false, // اجعلها console.log لترى SQL المولَّد}); module.exports = sequelize;
// models/Book.js: نموذج = جدولconst { DataTypes } = require('sequelize');const sequelize = require('../db'); const Book = sequelize.define('Book', { title: { type: DataTypes.STRING, allowNull: false }, author: { type: DataTypes.STRING, allowNull: false }, year: { type: DataTypes.INTEGER, validate: { min: 1900, max: 2100 } }, available: { type: DataTypes.BOOLEAN, defaultValue: true },}); module.exports = Book;
كل كتاب يعود ومعه 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();
// db.js: الاتصال بقاعدة البيانات (SQLite في ملف؛ وفي الإنتاج MySQL أو PostgreSQL)const { Sequelize } = require('sequelize'); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'library.sqlite', logging: false, // اجعلها console.log لترى SQL المولَّد}); module.exports = sequelize;
// models/Book.js: نموذج = جدولconst { DataTypes } = require('sequelize');const sequelize = require('../db'); const Book = sequelize.define('Book', { title: { type: DataTypes.STRING, allowNull: false }, author: { type: DataTypes.STRING, allowNull: false }, year: { type: DataTypes.INTEGER, validate: { min: 1900, max: 2100 } }, available: { type: DataTypes.BOOLEAN, defaultValue: true },}); module.exports = Book;
| Op | SQL | مثال |
|---|---|---|
Op.gt / Op.gte | > / >= | { price: { [Op.gt]: 100 } } |
Op.lt / Op.lte | < / <= | { year: { [Op.lt]: 2000 } } |
Op.ne | != | { status: { [Op.ne]: 'ملغى' } } |
Op.in | IN (…) | { id: { [Op.in]: [1, 2, 5] } } |
Op.like | LIKE | { 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.الخلاصة
ORM: جدول = نموذج، وصف = كائن، واستعلام = دالة.sequelize.define+DataTypes+allowNull/unique/validate/defaultValue.findAllوfindByPkوcreateوupdateوdestroy، كلهاasync.whereمعOp، وorderوlimitوattributes.ValidationError← 400، وUniqueConstraintError← 409.
تحدٍّ برمجي: مخزن المنتجات
متوسطخادم منتجات يحفظ بياناته في قاعدة بيانات عبر 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 إن لم يوجد.
👀 اعرض الاستجابات المطلوبة
POST /api/products{"name": "شاشة", "price": 899.5, "stock": 4}POST /api/products{"name": "كابل", "price": 25}POST /api/products{"name": "فأرة", "price": 60}POST /api/products{"name": "سعر سالب", "price": -5}POST /api/products{"price": 10}GET /api/products?max=100DELETE /api/products/2DELETE /api/products/2GET /api/products
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- إنشاء منتج ← 201
- … والرد فيه المنتج و
stock = 4 - stock افتراضيًّا 0
- السعر السالب ← 400
- بلا اسم ← 400
max=100يعيد الكابل ثم الفأرة فقط- … منتجين بالضبط
- الحذف ← 204
- حذف غير الموجود ← 404
- بقي منتجان
💡 تلميح 1
validate: { min: 0 } على السعر، و defaultValue: 0 على المخزون.💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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));