MongoDB مع Mongoose
مقدمة إلى MongoDB و Mongoose
MongoDB هي قاعدة بيانات مستندات NoSQL تخزن البيانات في مستندات مرنة تشبه JSON. Mongoose هي مكتبة Object Data Modeling (ODM) لـ MongoDB و Node.js توفر حلاً قائماً على المخططات لنمذجة بيانات التطبيق مع تحويل الأنواع المدمج، والتحقق من الصحة، وبناء الاستعلامات، وخطافات منطق الأعمال.
لماذا MongoDB مع Node.js؟
- كلاهما يستخدم JavaScript/JSON للبيانات
- مخطط مرن للتطوير السريع
- قابلية التوسع الأفقي
- لغة استعلام غنية وإطار تجميع
- مجتمع ونظام بيئي قوي
إعداد MongoDB و Mongoose
أولاً، قم بتثبيت MongoDB محلياً أو استخدم MongoDB Atlas (خدمة سحابية)، ثم قم بتثبيت Mongoose في مشروعك:
# تثبيت Mongoose
npm install mongoose
# لمتغيرات البيئة
npm install dotenv
أنشئ ملف تكوين قاعدة البيانات:
// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
قم بتحديث ملف الخادم الرئيسي للاتصال بـ MongoDB:
// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/database');
const app = express();
// الاتصال بـ MongoDB
connectDB();
app.use(express.json());
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
تنسيق سلسلة الاتصال: في ملف .env الخاص بك، استخدم:
MONGODB_URI=mongodb://localhost:27017/myapp لـ MongoDB المحلي، أو
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/myapp لـ MongoDB Atlas.
إنشاء مخططات ونماذج Mongoose
تحدد المخططات بنية المستندات داخل مجموعة. النماذج هي مُنشئات مجمعة من المخططات تنشئ وتقرأ المستندات.
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'الاسم مطلوب'],
trim: true,
minlength: [2, 'يجب أن يكون الاسم حرفين على الأقل'],
maxlength: [50, 'لا يمكن أن يتجاوز الاسم 50 حرفاً']
},
email: {
type: String,
required: [true, 'البريد الإلكتروني مطلوب'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'الرجاء إدخال بريد إلكتروني صالح']
},
password: {
type: String,
required: [true, 'كلمة المرور مطلوبة'],
minlength: [6, 'يجب أن تكون كلمة المرور 6 أحرف على الأقل'],
select: false // لا تُضمّن في الاستعلامات افتراضياً
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
age: {
type: Number,
min: [18, 'يجب أن يكون العمر 18 سنة على الأقل'],
max: [120, 'يجب أن يكون العمر واقعياً']
},
isActive: {
type: Boolean,
default: true
},
avatar: String,
tags: [String],
createdAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true // يضيف createdAt و updatedAt تلقائياً
});
// إنشاء وتصدير النموذج
const User = mongoose.model('User', userSchema);
module.exports = User;
خيارات المخطط:
required: يجب أن يحتوي الحقل على قيمةunique: ينشئ فهرساً فريداًdefault: القيمة الافتراضية إذا لم تُقدمenum: يجب أن تكون القيمة من القائمة المحددةmin/max: القيم الدنيا/القصوىmatch: يجب أن تتطابق مع نمط regextrim: إزالة المسافات البيضاءlowercase/uppercase: تحويل الحالة
عمليات CRUD مع Mongoose
إنشاء المستندات
// إنشاء مستند واحد
const createUser = async (req, res) => {
try {
const user = await User.create({
name: 'جون دو',
email: 'john@example.com',
password: 'password123',
age: 25
});
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// طريقة بديلة
const createUserAlt = async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// إدراج مستندات متعددة
const createMultipleUsers = async (req, res) => {
try {
const users = await User.insertMany([
{ name: 'مستخدم 1', email: 'user1@example.com', password: 'pass123' },
{ name: 'مستخدم 2', email: 'user2@example.com', password: 'pass123' }
]);
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.find();
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.find({ isActive: true });
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.findById(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({ 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 updateUser = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{
new: true, // إرجاع المستند المحدث
runValidators: true // تشغيل التحققات من المخطط
}
);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// تحديث واحد مع الشروط
const updateUserEmail = async (req, res) => {
try {
const result = await User.updateOne(
{ _id: req.params.id },
{ email: req.body.email }
);
res.status(200).json({
success: true,
data: result
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// تحديث مستندات متعددة
const deactivateOldUsers = async (req, res) => {
try {
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const result = await User.updateMany(
{ createdAt: { $lt: sixMonthsAgo }, isActive: true },
{ isActive: false }
);
res.status(200).json({
success: true,
modifiedCount: result.modifiedCount
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
حذف المستندات
// الحذف بواسطة المعرف
const deleteUser = async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// حذف واحد مع الشروط
const deleteInactiveUser = async (req, res) => {
try {
const result = await User.deleteOne({
_id: req.params.id,
isActive: false
});
if (result.deletedCount === 0) {
return res.status(404).json({
success: false,
error: 'لا يوجد مستخدم غير نشط بهذا المعرف'
});
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// حذف مستندات متعددة
const deleteInactiveUsers = async (req, res) => {
try {
const result = await User.deleteMany({ isActive: false });
res.status(200).json({
success: true,
deletedCount: result.deletedCount
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
الحذف مقابل الإزالة: الطريقة remove() مهملة. استخدم deleteOne()، أو deleteMany()، أو findByIdAndDelete() بدلاً من ذلك.
الاستعلامات المتقدمة
// معاملات الاستعلام
const advancedQuery = async (req, res) => {
try {
// معاملات المقارنة
const adults = await User.find({ age: { $gte: 18 } });
const youngAdults = await User.find({ age: { $gte: 18, $lt: 30 } });
const specificAges = await User.find({ age: { $in: [20, 25, 30] } });
// المعاملات المنطقية
const activeAdmins = await User.find({
$and: [
{ role: 'admin' },
{ isActive: true }
]
});
const adminOrModerator = await User.find({
$or: [
{ role: 'admin' },
{ role: 'moderator' }
]
});
// معاملات السلسلة
const usersStartingWithJ = await User.find({
name: { $regex: '^J', $options: 'i' }
});
// معامل الوجود
const usersWithAvatar = await User.find({ avatar: { $exists: true } });
// معاملات المصفوفة
const usersWithTag = await User.find({ tags: 'javascript' });
const usersWithAllTags = await User.find({
tags: { $all: ['javascript', 'nodejs'] }
});
res.status(200).json({
success: true,
data: {
adults: adults.length,
youngAdults: youngAdults.length,
activeAdmins: activeAdmins.length
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// الفرز والتحديد واختيار الحقول
const getUsersFiltered = async (req, res) => {
try {
const users = await User
.find({ isActive: true })
.select('name email age -_id') // تضمين name و email و age؛ استبعاد _id
.sort({ age: -1 }) // الفرز حسب العمر تنازلياً
.limit(10) // تحديد النتائج بـ 10
.skip(0); // تخطي أول 0 (الترقيم)
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// الترقيم
const getUsersPaginated = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User
.find()
.limit(limit)
.skip(skip)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
res.status(200).json({
success: true,
count: users.length,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
},
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
التعبئة (الإشارة إلى المستندات)
تتيح لك التعبئة الإشارة إلى المستندات في مجموعات أخرى واستبدال المسارات المحددة تلقائياً بالمستندات من مجموعات أخرى.
// models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment'
}],
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
}, {
timestamps: true
});
const Post = mongoose.model('Post', postSchema);
module.exports = Post;
// استخدام التعبئة
const getPostWithAuthor = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name email'); // تعبئة المؤلف، اختيار الاسم والبريد الإلكتروني فقط
if (!post) {
return res.status(404).json({
success: false,
error: 'المنشور غير موجود'
});
}
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// تعبئة متعددة
const getPostWithDetails = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name email avatar')
.populate('comments')
.populate('likes', 'name');
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// التعبئة المتداخلة
const getPostWithCommentAuthors = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name')
.populate({
path: 'comments',
populate: {
path: 'author',
select: 'name avatar'
}
});
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
تمرين تطبيقي:
- أنشئ نموذج User بحقول name و email و password و role
- نفذ عمليات CRUD للمستخدمين
- أضف مرشحات استعلام للحصول على المستخدمين حسب الدور ونطاق العمر
- أنشئ نموذج Post مع إشارة إلى User كمؤلف
- نفذ التعبئة لجلب المنشورات مع تفاصيل المؤلف
- أضف الترقيم إلى نقطة النهاية للحصول على جميع المستخدمين
- أنشئ نقطة نهاية للبحث عن المستخدمين بالاسم باستخدام regex
أفضل الممارسات:
- استخدم الفهارس للحقول المستعلمة بشكل متكرر
- لا تُفرط في التعبئة؛ اختر الحقول المطلوبة فقط
- استخدم lean() للاستعلامات للقراءة فقط (أسرع)
- تعامل مع أخطاء التحقق بشكل صحيح
- استخدم المعاملات للعمليات التي تؤثر على مجموعات متعددة
- أنشئ ملفات منفصلة لكل نموذج
- استخدم طرق المخطط للمنطق القابل لإعادة الاستخدام