قواعد بيانات SQL مع Sequelize
مقدمة إلى Sequelize ORM
Sequelize هي أداة ORM (Object-Relational Mapping) قائمة على الوعود لـ Node.js تدعم PostgreSQL و MySQL و MariaDB و SQLite و Microsoft SQL Server. تتميز بدعم قوي للمعاملات، والعلاقات، والتحميل الحثيث والكسول، وتكرار القراءة، والمزيد. يوفر Sequelize طبقة تجريد قوية فوق SQL الخام، مما يسمح لك بالعمل مع قواعد البيانات باستخدام كائنات JavaScript.
لماذا استخدام Sequelize؟
- كود محايد لقاعدة البيانات (تبديل قواعد البيانات بسهولة)
- توليد استعلامات SQL تلقائياً
- التحقق من الصحة المدمج وأنواع البيانات
- دعم الترحيل والبذر
- إدارة المعاملات
- معالجة العلاقات (الارتباطات)
- دعم سلامة الأنواع و intellisense
تثبيت وتكوين Sequelize
# تثبيت Sequelize ومحرك قاعدة البيانات
npm install sequelize
# اختر محرك قاعدة البيانات الخاص بك
npm install pg pg-hstore # PostgreSQL
npm install mysql2 # MySQL
npm install mariadb # MariaDB
npm install sqlite3 # SQLite
npm install tedious # Microsoft SQL Server
# تثبيت Sequelize CLI للترحيلات
npm install --save-dev sequelize-cli
تهيئة Sequelize في مشروعك:
# تهيئة Sequelize (ينشئ مجلدات config و models و migrations و seeders)
npx sequelize-cli init
تكوين اتصال قاعدة البيانات في config/config.json:
{
"development": {
"username": "root",
"password": "password",
"database": "myapp_dev",
"host": "127.0.0.1",
"dialect": "mysql",
"logging": console.log
},
"test": {
"username": "root",
"password": "password",
"database": "myapp_test",
"host": "127.0.0.1",
"dialect": "mysql",
"logging": false
},
"production": {
"username": process.env.DB_USERNAME,
"password": process.env.DB_PASSWORD,
"database": process.env.DB_DATABASE,
"host": process.env.DB_HOST,
"dialect": "mysql",
"logging": false,
"pool": {
"max": 5,
"min": 0,
"acquire": 30000,
"idle": 10000
}
}
}
إنشاء مثيل اتصال قاعدة البيانات:
// config/database.js
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(
process.env.DB_DATABASE,
process.env.DB_USERNAME,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
dialect: 'mysql', // أو 'postgres', 'sqlite', 'mariadb', 'mssql'
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
// اختبار الاتصال
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('تم إنشاء اتصال قاعدة البيانات بنجاح.');
} catch (error) {
console.error('غير قادر على الاتصال بقاعدة البيانات:', error);
}
};
testConnection();
module.exports = sequelize;
متغيرات البيئة: خزّن بيانات اعتماد قاعدة البيانات في ملف .env:
DB_HOST=localhost
DB_USERNAME=root
DB_PASSWORD=yourpassword
DB_DATABASE=myapp_db
تعريف النماذج
تمثل النماذج الجداول في قاعدة البيانات الخاصة بك. كل نموذج يتوافق مع جدول، ومثيلات النموذج تمثل صفوفاً في ذلك الجدول.
// models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
validate: {
len: [3, 50],
notEmpty: true
}
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true,
notEmpty: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
len: [6, 255]
}
},
firstName: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'first_name' // اسم العمود في قاعدة البيانات
},
lastName: {
type: DataTypes.STRING(50),
allowNull: true,
field: 'last_name'
},
age: {
type: DataTypes.INTEGER,
allowNull: true,
validate: {
min: 18,
max: 120
}
},
role: {
type: DataTypes.ENUM('user', 'admin', 'moderator'),
defaultValue: 'user'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
field: 'is_active'
},
lastLogin: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login'
}
}, {
tableName: 'users',
timestamps: true, // يضيف createdAt و updatedAt
underscored: true, // استخدم snake_case للحقول المضافة تلقائياً
paranoid: true, // يضيف deletedAt للحذف الناعم
indexes: [
{
unique: true,
fields: ['email']
},
{
fields: ['username']
}
]
});
module.exports = User;
أنواع البيانات الشائعة:
DataTypes.STRING(length)- VARCHARDataTypes.TEXT- TEXTDataTypes.INTEGER- INTEGERDataTypes.BIGINT- BIGINTDataTypes.FLOAT- FLOATDataTypes.DECIMAL(precision, scale)- DECIMALDataTypes.BOOLEAN- TINYINT(1)DataTypes.DATE- DATETIMEDataTypes.DATEONLY- DATEDataTypes.ENUM(...values)- ENUMDataTypes.JSON- JSON (MySQL, PostgreSQL)DataTypes.UUID- UUID (PostgreSQL)
الترحيلات
الترحيلات هي طريقة لإجراء تغييرات على مخطط قاعدة البيانات الخاصة بك بمرور الوقت بطريقة منهجية وقابلة للإلغاء.
# إنشاء ترحيل
npx sequelize-cli migration:generate --name create-users-table
تحرير ملف الترحيل المُنشأ:
// migrations/YYYYMMDDHHMMSS-create-users-table.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
username: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true
},
email: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true
},
password: {
type: Sequelize.STRING(255),
allowNull: false
},
first_name: {
type: Sequelize.STRING(50),
allowNull: true
},
last_name: {
type: Sequelize.STRING(50),
allowNull: true
},
age: {
type: Sequelize.INTEGER,
allowNull: true
},
role: {
type: Sequelize.ENUM('user', 'admin', 'moderator'),
defaultValue: 'user'
},
is_active: {
type: Sequelize.BOOLEAN,
defaultValue: true
},
last_login: {
type: Sequelize.DATE,
allowNull: true
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
},
deleted_at: {
type: Sequelize.DATE,
allowNull: true
}
});
// إضافة الفهارس
await queryInterface.addIndex('users', ['email'], {
unique: true,
name: 'users_email_unique'
});
await queryInterface.addIndex('users', ['username'], {
name: 'users_username_index'
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users');
}
};
تشغيل الترحيلات:
# تشغيل جميع الترحيلات المعلقة
npx sequelize-cli db:migrate
# التراجع عن آخر ترحيل
npx sequelize-cli db:migrate:undo
# التراجع عن جميع الترحيلات
npx sequelize-cli db:migrate:undo:all
# التحقق من حالة الترحيل
npx sequelize-cli db:migrate:status
مهم: لا تعدّل أبداً الترحيلات الموجودة التي تم تشغيلها بالفعل. أنشئ ترحيلات جديدة للتغييرات للحفاظ على التحكم في الإصدار وتجنب فقدان البيانات.
عمليات CRUD
إنشاء السجلات
// إنشاء سجل واحد
const createUser = async (req, res) => {
try {
const user = await User.create({
username: 'johndoe',
email: 'john@example.com',
password: 'hashedpassword',
firstName: 'جون',
lastName: 'دو',
age: 25
});
res.status(201).json({
success: true,
data: user
});
} catch (error) {
if (error.name === 'SequelizeValidationError') {
return res.status(400).json({
success: false,
errors: error.errors.map(e => ({
field: e.path,
message: e.message
}))
});
}
res.status(500).json({
success: false,
error: error.message
});
}
};
// البناء ولكن لا حفظ
const buildUser = async () => {
const user = User.build({
username: 'janedoe',
email: 'jane@example.com'
});
// الحفظ لاحقاً
await user.save();
};
// الإنشاء الجماعي
const createMultipleUsers = async (req, res) => {
try {
const users = await User.bulkCreate([
{ username: 'user1', email: 'user1@example.com', password: 'pass123' },
{ username: 'user2', email: 'user2@example.com', password: 'pass123' }
], {
validate: true, // تشغيل التحققات
ignoreDuplicates: false // رمي خطأ في حالة التكرارات
});
res.status(201).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
قراءة السجلات
// البحث عن الكل
const getAllUsers = async (req, res) => {
try {
const users = await User.findAll();
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// البحث مع الشروط
const getActiveUsers = async (req, res) => {
try {
const users = await User.findAll({
where: {
isActive: true
},
attributes: ['id', 'username', 'email', 'role'], // اختيار حقول محددة
order: [['createdAt', 'DESC']], // الفرز
limit: 10,
offset: 0
});
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// البحث بواسطة المفتاح الأساسي
const getUserById = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// البحث عن واحد مع الشروط
const getUserByEmail = async (req, res) => {
try {
const user = await User.findOne({
where: {
email: req.params.email
}
});
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// العد
const countUsers = async (req, res) => {
try {
const count = await User.count({
where: {
isActive: true
}
});
res.status(200).json({
success: true,
count
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
تحديث السجلات
// تحديث سجل واحد
const updateUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
await user.update(req.body);
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// التحديث مع الشروط
const updateUserStatus = async (req, res) => {
try {
const [updatedCount] = await User.update(
{ isActive: false },
{
where: {
id: req.params.id
}
}
);
if (updatedCount === 0) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
message: 'تم تحديث المستخدم بنجاح'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// الزيادة/النقصان
const incrementAge = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
await user.increment('age', { by: 1 });
// أو user.decrement('age', { by: 1 });
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
حذف السجلات
// الحذف الناعم (إذا كان paranoid مُفعّلاً)
const deleteUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
await user.destroy(); // يضبط طابع الوقت deletedAt
res.status(200).json({
success: true,
message: 'تم حذف المستخدم بنجاح'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// الحذف الصارم
const forceDeleteUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
await user.destroy({ force: true }); // الحذف الدائم
res.status(200).json({
success: true,
message: 'تم حذف المستخدم نهائياً'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// الحذف مع الشروط
const deleteInactiveUsers = async (req, res) => {
try {
const deletedCount = await User.destroy({
where: {
isActive: false
}
});
res.status(200).json({
success: true,
deletedCount
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// استعادة المحذوف ناعماً
const restoreUser = async (req, res) => {
try {
await User.restore({
where: {
id: req.params.id
}
});
res.status(200).json({
success: true,
message: 'تمت استعادة المستخدم بنجاح'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
الاستعلامات المتقدمة
const { Op } = require('sequelize');
// المعاملات
const advancedQueries = async () => {
// المقارنة
const adults = await User.findAll({
where: {
age: { [Op.gte]: 18 }
}
});
const youngAdults = await User.findAll({
where: {
age: {
[Op.gte]: 18,
[Op.lt]: 30
}
}
});
const specificAges = await User.findAll({
where: {
age: { [Op.in]: [20, 25, 30] }
}
});
// مطابقة السلسلة
const usersStartingWithJ = await User.findAll({
where: {
username: { [Op.like]: 'J%' }
}
});
const usersContainingDoe = await User.findAll({
where: {
username: { [Op.iLike]: '%doe%' } // غير حساس لحالة الأحرف (PostgreSQL)
}
});
// المعاملات المنطقية
const activeAdmins = await User.findAll({
where: {
[Op.and]: [
{ role: 'admin' },
{ isActive: true }
]
}
});
const adminOrModerator = await User.findAll({
where: {
[Op.or]: [
{ role: 'admin' },
{ role: 'moderator' }
]
}
});
// استعلامات التاريخ
const recentUsers = await User.findAll({
where: {
createdAt: {
[Op.gte]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // آخر 7 أيام
}
}
});
// فحوصات null
const usersWithoutLastName = await User.findAll({
where: {
lastName: { [Op.is]: null }
}
});
const usersWithLastName = await User.findAll({
where: {
lastName: { [Op.not]: null }
}
});
};
تمرين تطبيقي:
- أعد إعداد Sequelize مع PostgreSQL أو MySQL في مشروع Node.js
- أنشئ نموذج User مع قواعد التحقق من الصحة
- أنشئ وشغّل ترحيل لجدول المستخدمين
- نفذ عمليات CRUD للمستخدمين
- أضف مرشحات استعلام متقدمة (نطاق العمر، الدور، الحالة النشطة)
- نفذ الترقيم لنقطة النهاية للحصول على جميع المستخدمين
- أنشئ بادر لملء البيانات الأولية
- نفذ وظيفة الحذف الناعم والاستعادة
أفضل الممارسات:
- استخدم الترحيلات لجميع تغييرات المخطط
- أضف فهارس للأعمدة المستعلمة بشكل متكرر
- استخدم المعاملات للعمليات التي تؤثر على جداول متعددة
- تحقق من البيانات على مستوى النموذج
- استخدم وضع paranoid للحذف الناعم عند الاقتضاء
- حافظ على الاستعلامات الخام في حدها الأدنى؛ استخدم طرق Sequelize
- استخدم تكوينات خاصة بالبيئة
- سجّل الاستعلامات في التطوير، عطّلها في الإنتاج