معرفة MongoDB من جهة mongosh هي نصف العمل فقط: في الإنتاج، يفتح المُشغِّل (driver) الاتصالات من جهة التطبيق، ويُدير الـ pool، ويُعيد محاولة الكتابات، ويعرض جلسة المعاملات. على Node.js: المُشغّل الأصلي mongodb والـ ODM المسمّى mongoose. على Python: المُشغّل المتزامن pymongo ونظيره غير المتزامن motor. يشرح هذا الدليل كيف تُهيِّئ كل ثنائية على MongoDB 8.0 LTS لتحمّل الحِمل وتجاوز failover replica set، مع احترام ممارسات الـ pool والـ timeout والـ idempotence.
المتطلّبات
- MongoDB 8.0 LTS — replica set أو Atlas M0 كحدّ أدنى.
- Node.js 22 LTS أو أحدث لقسم Node — Node 20 يبلغ نهاية الدعم في أبريل 2026.
- Python 3.12 أو أحدث لقسم Python.
- مستوى متوسّط: تكتب أصلًا كودًا غير متزامن (Promise/async/await في Node، asyncio في Python).
- الوقت المتوقّع: 110 دقيقة لاستكمال الخطوات التسع.
الخطوة 1 — اختيار بين المُشغّل الأصلي وODM
المُشغّل هو الطبقة منخفضة المستوى التي تتحدّث البروتوكول الثنائي لـ MongoDB: يفتح اتصالات TCP، يُسلسل المستندات إلى BSON، ويعرض عمليات CRUD دون فرض أيّ شيء على التطبيق. أمّا الـ ODM (Object Document Mapper) فيُضيف فوق ذلك مخطّطًا مُكَتَّبًا، وخطّافات دورة حياة، وتحقّقًا، وعلاقات على طريقة الـ ORM.
على Node، المُشغّل الأصلي هو mongodb — وهو نفسه ما يستخدمه mongoose داخليًا. لديك إذًا خيار العمل به مباشرة (مُسهَب لكن مرن) أو عبر mongoose (مُختَصر لكن مُلزَم بقواعد). في 2026، المُشغّل mongodb في الإصدار 7.x، وmongoose في 9.x ويبقى متوافقًا مع المُشغّل 6.x أو 7.x. على Python، pymongo هو المُشغّل المتزامن الرسمي الذي تصونه MongoDB Inc.، وmotor هو نظيره الـ asyncio. لـ FastAPI وأي خلفية حديثة غير متزامنة، motor هو الخيار البديهي.
الخطوة 2 — التثبيت والربط في Node.js
التثبيت تافه عبر npm. الاتّصال يفتح connection pool مشتركًا — تُنشئ MongoClient مرّة واحدة فقط عند إقلاع التطبيق، لا مع كل طلب HTTP. إعادة إنشاء عميل مع كل طلب هو الخطأ رقم 1 لدى المبتدئين: يفتح الـ pool عشرات اتصالات TCP مع مصافحة TLS، ممّا يدمّر زمن الاستجابة ويُسبّب تسرّب واصفات الملفات.
// package.json
// npm install mongodb@7
import { MongoClient } from "mongodb";
const uri = process.env.MONGO_URI;
// صيغة URI لـ replica set:
// mongodb://user:pass@node1,node2,node3/dbname?replicaSet=rs0&authSource=admin
const client = new MongoClient(uri, {
maxPoolSize: 50,
minPoolSize: 5,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 10000,
socketTimeoutMS: 30000,
retryWrites: true,
retryReads: true,
w: "majority"
});
await client.connect();
const db = client.db("ecom");
const produits = db.collection("produits");
// عند الإيقاف النظيف للعملية
process.on("SIGTERM", async () => { await client.close(); process.exit(0); });
خمسة خيارات يجب حفظها. maxPoolSize: 50 سقف موصى به لمعظم تطبيقات الويب — فوق ذلك يستهلك الخادم خيوطًا دون فائدة. minPoolSize: 5 يُبقي 5 اتصالات ساخنة لامتصاص الذرى. retryWrites: true يُعيد المحاولة تلقائيًا مرّة للكتابات المُحايدة عند انقطاع الشبكة أو تبديل الـ primary. w: "majority" ينتظر إقرار الأغلبية قبل اعتبار الكتابة مُؤكَّدة — هذه هي المتانة الحقيقية، وليس مجرّد قبول على الـ primary وحده.
الخطوة 3 — Mongoose: مخطّط، نموذج، تحقّق
يُضيف Mongoose مخطّطًا مُكَتَّبًا على Node. الميزة: تحقّق عند الكتابة، إكمال تلقائي على TypeScript، خطّافات pre/post على العمليات. العيب: طبقة تجريد قد تُخفي السلوك الفعلي للمُشغّل — معرفته مفيدة لتنقيح الحالات الحدية.
// npm install mongoose@9
import mongoose, { Schema, model } from "mongoose";
await mongoose.connect(process.env.MONGO_URI, {
maxPoolSize: 50,
serverSelectionTimeoutMS: 5000,
retryWrites: true
});
const produitSchema = new Schema({
nom: { type: String, required: true, minlength: 2, maxlength: 200 },
prix_usd: { type: Number, required: true, min: 0 },
categorie: { type: String, enum: ["smartphone", "tissu", "alimentaire"] },
stock: { type: Number, default: 0, min: 0 },
attributs: { type: Schema.Types.Mixed },
cree_le: { type: Date, default: Date.now }
}, { timestamps: true });
produitSchema.index({ categorie: 1, prix_usd: -1 });
const Produit = model("Produit", produitSchema);
// CRUD
const nouveau = await Produit.create({ nom: "Test", prix_usd: 25, categorie: "tissu" });
const liste = await Produit.find({ categorie: "smartphone" }).limit(10).lean();
await Produit.updateOne({ _id: nouveau._id }, { $inc: { stock: 1 } });
الخيار .lean() يُرجع كائنات JavaScript خام بدل مستندات Mongoose مُهَيدَرة — مكسب ملموس في الأداء والذاكرة عندما لا تحتاج إلى الخطّافات ولا واجهة المستند. القاعدة العملية: .lean() على كل قراءة لا تستدعي .save() بعدها. المرجع: Mongoose Lean Queries.
الخطوة 4 — المعاملات مع Mongoose
تعتمد معاملة Mongoose على جلسة تُحصَّل من الاتصال. النمط مطابق لما يفعله المُشغّل الأصلي: البَدء، تنفيذ العمليات مع الجلسة، ثم commit أو abort.
const session = await mongoose.startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
await Compte.updateOne(
{ _id: idDebit, solde: { $gte: 10000 } },
{ $inc: { solde: -10000 } },
{ session }
);
await Compte.updateOne(
{ _id: idCredit },
{ $inc: { solde: +10000 } },
{ session }
);
await Operation.create([{
type: "virement",
debit: idDebit,
credit: idCredit,
montant: 10000
}], { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}
الشرط solde: { $gte: 10000 } في فلتر أول updateOne جوهري: يضمن ذرّيًّا أنّ للحساب الرصيد الكافي للخصم. إذا كان الرصيد قد خُصم سلفًا بعملية متزامنة، يُرجع الـ updateOne قيمة matchedCount: 0 — على كودك أن يكتشف ذلك ويُلغي المعاملة. بدون هذا الشرط، تخاطر برصيد سالب: تنجح المعاملة لكنّ النتيجة التجارية خاطئة.
الخطوة 5 — PyMongo: المُشغّل المتزامن لـ Python
على Python، pymongo هو المُشغّل المرجعي لسكربتات الـ batch وأوراق Jupyter ومهامّ cron. إنّه متزامن — كلّ عملية تحجب حتى يصل الردّ. مثالي لـ ETL والتحليل، أقلّ ملاءمة لخلفيّات الويب المتزامنة بكثرة.
# pip install pymongo[srv]
from pymongo import MongoClient, WriteConcern, ReadPreference
from pymongo.errors import DuplicateKeyError, AutoReconnect
import os
client = MongoClient(
os.environ["MONGO_URI"],
maxPoolSize=50,
minPoolSize=5,
serverSelectionTimeoutMS=5000,
connectTimeoutMS=10000,
socketTimeoutMS=30000,
retryWrites=True,
retryReads=True,
w="majority"
)
db = client["ecom"]
produits = db["produits"]
# CRUD أساسي
produits.insert_one({"nom": "Test", "prix_usd": 25, "categorie": "tissu"})
# قراءة مع readPreference موجَّهة نحو الـ secondaries
produits_lus = list(
db.get_collection("produits", read_preference=ReadPreference.SECONDARY_PREFERRED)
.find({"actif": True})
.limit(10)
)
اللاحقة pymongo[srv] عند التثبيت تُضيف اعتمادية dnspython الضرورية لروابط mongodb+srv:// النموذجية لـ Atlas — بدونها يفشل الاتصال برسالة غامضة. القاعدة العملية: ثبِّت دائمًا pymongo[srv] ما لم تتأكّد أنّك لن تستخدم SRV أبدًا. المرجع: PyMongo Installation.
الخطوة 6 — Motor: مُشغّل asyncio
لـ FastAPI وStarlette وأي خلفية Python غير متزامنة، يحلّ motor محلّ pymongo. الواجهة متطابقة بنسبة 95%، بشرط إضافة await أمام كلّ عملية تلامس الشبكة. motor تصونه MongoDB Inc. ويبقى المسار الرسمي لـ Python async.
# pip install motor
from motor.motor_asyncio import AsyncIOMotorClient
import os, asyncio
client = AsyncIOMotorClient(
os.environ["MONGO_URI"],
maxPoolSize=50,
serverSelectionTimeoutMS=5000,
retryWrites=True
)
db = client["ecom"]
produits = db["produits"]
async def lister_smartphones():
cursor = produits.find({"categorie": "smartphone", "actif": True}).limit(20)
return await cursor.to_list(length=20)
# دمج FastAPI (lifespan الموصى به منذ FastAPI 0.93)
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# فرض فتح الـ pool عند الإقلاع
await client.admin.command("ping")
yield
# إغلاق نظيف عند الإيقاف
client.close()
app = FastAPI(lifespan=lifespan)
@app.get("/produits")
async def get_produits():
return await lister_smartphones()
دالّة lifespan عند الإقلاع تفصيل كثيرًا ما يُنسى (منذ FastAPI 0.93 صار @app.on_event("startup") مُلغًى لصالح lifespan context manager): بدونها ينتظر أوّل عميل يطرق الـ API إنشاء الـ pool ويختبر تأخّرًا غير طبيعي. ping مبدئي يستدعي فتح الاتصالات وتحديد الـ primary؛ الطلبات اللاحقة تستفيد من pool ساخن جاهز.
الخطوة 7 — Pool الاتصالات: قياس صحيح
الـ pool هو خزّان sockets قابلة لإعادة الاستعمال. صغير جدًّا يُصبح عنق الزجاجة عند ارتفاع حركة المرور. كبير جدًّا يُشبع الخادم بـ sockets مفتوحة ويستبعد تطبيقات أخرى. الحجم الأمثل يعتمد على الحركة وتزامن التطبيق.
| الحركة النموذجية | maxPoolSize | minPoolSize | ملاحظات |
|---|---|---|---|
| API CRUD بسيط، < 100 req/s | 20 | 2 | افتراضي محافظ، يكفي MVP |
| API تجارة إلكترونية، 100-1000 req/s | 50 | 5 | الملف الموصى به لمعظم الحالات |
| API تحليلات، تجميعات طويلة | 100 | 10 | لتعويض زمن الردّ الأطول |
| Workers معالجة batch | 200 | 20 | إن كانت الـ workers تُوازي بشدّة |
للحساب الدقيق: poolSize ≈ طلبات_في_الثانية × زمن_الاستجابة_المتوسّط_بالثواني. عند 500 req/s مع 80 ms زمن استجابة متوسّط نحتاج 40 اتصالًا نشطًا في الذروة — maxPoolSize: 50 يُعطي هامشًا معقولًا. التوسيع دون قياس مُضرّ.
الخطوة 8 — معالجة أخطاء الشبكة وfailover
في الإنتاج على replica set، قد يتبدّل الـ primary: صيانة، عطل عتاد، انقطاع شبكة. على المُشغّل أن يكتشف ويُعيد المحاولة ويتّصل من جديد دون تدخّل. ثلاث آليّات تتعاون.
أوّلًا، server discovery: يحتفظ المُشغّل بصورة محدَّثة لطوبولوجيا الـ replica set عبر heartbeats. عند اختفاء الـ primary، يكتشف الانتخاب الجاري ويُعلِّق الكتابات. ثانيًا، retryable writes (retryWrites: true): العمليات المُحايدة (insertOne، updateOne مع $set، deleteOne) يُعاد محاولتها تلقائيًا مرّة واحدة. ثالثًا، retryable reads (retryReads: true): الآليّة نفسها للقراءات.
// Node.js — معالجة صريحة لخطأ الشبكة
import { MongoNetworkError, MongoServerSelectionError } from "mongodb";
async function inserer(doc, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await produits.insertOne(doc);
} catch (err) {
if (err instanceof MongoNetworkError || err instanceof MongoServerSelectionError) {
await new Promise(r => setTimeout(r, 200 * (i + 1)));
continue;
}
throw err;
}
}
throw new Error("MongoDB غير متاح بعد 3 محاولات");
}
لاحظ الـ backoff الخطّي (200 * (i + 1) ms) بين كلّ محاولة. بدون backoff، تُغرق الخادم بالمحاولات وهو يحاول انتخاب primary جديد — تُطيل الحادثة. backoff أسّي (200 * 2^i) أكثر حذرًا في الحوادث الطويلة.
الخطوة 9 — السجلّات، المراقبة، المسبارات
مُشغّل مُهَيَّأ جيّدًا بلا مراقبة أعمى أمام الحوادث الفعلية. ثلاثة مصادر إشارة: السجلّات المُهيكَلة للمُشغّل، أوامر serverStatus وcurrentOp على الخادم، وتكامل مع Atlas أو عامل Prometheus للنشر الذاتي.
// Node.js — تجهيز OpenTelemetry لعمليات Mongo
import { MongoClient } from "mongodb";
import { MongoDBInstrumentation } from "@opentelemetry/instrumentation-mongodb";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
const provider = new NodeTracerProvider();
provider.register();
registerInstrumentations({
instrumentations: [
new MongoDBInstrumentation({ enhancedDatabaseReporting: true })
]
});
const client = new MongoClient(uri);
await client.connect();
// كلّ عملية تُنتج الآن span مع:
// - db.statement, db.collection, db.operation
// - المدّة، حالة النجاح/الإخفاق
# Python — مُصدِّر Prometheus لـ pool MongoDB
from prometheus_client import Gauge, start_http_server
pool_size = Gauge("mongodb_pool_size", "اتصالات نشطة في الـ pool")
start_http_server(9090)
# تحديث دوري
import asyncio
async def refresh_metrics():
while True:
status = await client.admin.command("serverStatus")
pool_size.set(status["connections"]["current"])
await asyncio.sleep(15)
asyncio.create_task(refresh_metrics())
ثلاثة مقاييس يجب تتبّعها في الإنتاج. connections.current: إن اقتربت من maxPoolSize فالـ pool مُشبَع ويجب توسيعه أو تخفيف الحِمل. opLatencies.reads.latency: إن تجاوزت 50 ms متوسّطًا، فالاستعلامات غير مُفَهرسة أو الخادم تحت الحجم. repl.lag: إذا تأخّر secondary بضع ثوانٍ عن الـ primary، يخرج من الـ quorum للكتابات w: majority ويرتفع زمن الكتابات الحرجة.
الأخطاء الشائعة
| الخطأ | السبب | الحلّ |
|---|---|---|
| Pool مُشبَع تحت حركة عادية | maxPoolSize منخفض أو إعادة إنشاء العميل مع كل طلب |
تأكّد أنّ MongoClient singleton، ارفع إلى 50 |
| ServerSelectionTimeoutError عند الإقلاع | URI سيّء التكوين أو DNS SRV لم يُحلّ | ثبّت pymongo[srv] أو dnspython، تحقّق من تحليل DNS |
| WriteConflict متكرّر في المعاملات | تنافس على نفس المستندات | حلقة retry مع backoff، أو نمط optimistic locking |
| قراءات ببيانات قديمة | readPreference: secondary دون maxStalenessSeconds |
افرض maxStalenessSeconds: 90 أو ارجع إلى الـ primary |
BulkWriteError على insertMany |
تكرار على فهرس فريد | استعمل ordered: false لئلّا يتوقّف كلّ شيء عند أوّل فشل |
| Mongoose يُهَيدر ملايين المستندات | قراءة بلا .lean() |
أضف .lean() أينما كنت تُسقِط الحقول فقط |
الأسئلة الشائعة
س: المُشغّل الأصلي mongodb أم ODM mongoose؟
ج: mongoose للتطبيقات القياسية بمخطّط ثابت وتحقّق قوي (مثالي لـ MVP وSaaS). المُشغّل الأصلي للأحمال المتقدّمة (pipelines معقّدة، أداء حرج، تحكّم دقيق في BSON).
س: PyMongo متزامن أم Motor غير متزامن؟
ج: PyMongo لسكربتات الـ batch وETL وjobs cron. Motor لـ FastAPI وأي خادم asyncio. القاعدة نفسها تكشف الاثنين — لا حاجة إلى إعادة كتابة كبرى للهجرة.
س: هل يجب إغلاق العميل مع كلّ طلب؟
ج: لا، أبدًا. العميل singleton يُفتح عند إقلاع العملية ويُغلق بنظافة على SIGTERM. الإغلاق وإعادة الفتح بين الطلبات يُلغي كلّ فائدة الـ pool.
س: كيف تمرّر ObjectId في URL؟
ج: الصيغة السداسية ذات 24 حرفًا تُمرَّر كما هي في URL. على الخادم، حوّلها صراحة: new ObjectId(req.params.id) في Node، ObjectId(id_str) في Python. دون هذا التحويل يبحث MongoDB عن سلسلة حيث ينتظر ObjectId ولا يجد شيئًا.
س: معاملات أم عمليات ذرّية؟
ج: فضّل الذرّي ما أمكن. update واحد بعوامل ($inc، $push، $addToSet) على مستند واحد ذرّي أصلًا وأسرع من معاملة. تبقى المعاملة لا غنى عنها حالما يجب أن يتطوّر مستندان مختلفان معًا.
للتعمّق
- MongoDB: NoSQL في الممارسة — نظرة عامّة على المسار.
- Replica sets MongoDB والانتخابات — فهم السياق الذي على المُشغّل إدارته.
- فهارس MongoDB والأداء — مُشغّل مُهَيَّأ جيّدًا يستفيد من فهارس مُحَكَمَة.
- Node.js Driver — Manual — التوثيق الرسمي.
- PyMongo Documentation — المرجع الرسمي لـ Python.
- Mongoose Documentation — الدليل الرسمي للـ ODM في Node.