تخطَّ إلى المحتوى

٢١ يوليو ٢٠٢٦

حل خطأ E11000 duplicate key error في MongoDB بالعربي

شارك المقال:

أي تطبيق يربط MongoDB بفهرس unique (بريد إلكتروني، اسم مستخدم، رقم طلب) يصطدم بهذه الرسالة عاجلًا أو آجلًا:

E11000 duplicate key error collection: store.users index: email_1
dup key: { email: "user@example.com" }

الرسالة أوضح ممّا تبدو — فيها كل ما تحتاجه للتشخيص: اسم المجموعة (store.users)، اسم الفهرس المخالَف (email_1)، والقيمة المكرَّرة نفسها.

لماذا يحدث أصلًا؟

الخطأ ناتج عن فهرس فريد (unique index) — أنشأته أنت أو مكتبة مثل Mongoose بناءً على تعريف المخطّط:

db.users.createIndex({ email: 1 }, { unique: true })

بمجرّد وجود هذا الفهرس، أي محاولة إدراج أو تحديث تنتج عنها قيمتان متطابقتان في حقل email تُرفَض فورًا — هذا هو الغرض من الفهرس أصلًا، وليس عطلًا في MongoDB.

السبب الأول: تكرار فعلي في البيانات

الحالة الأبسط: المستخدم يحاول التسجيل ببريد مسجَّل مسبقًا فعلًا.

try {
  await db.users.insertOne({ email: "user@example.com", name: "أحمد" });
} catch (err) {
  if (err.code === 11000) {
    // تعامل معه كخطأ منطقي متوقَّع، لا استثناءً يوقف التطبيق
    throw new Error("هذا البريد الإلكتروني مسجّل مسبقًا");
  }
  throw err;
}

💡 كود الخطأ الرقمي دائمًا 11000 — تحقّق منه في catch بدل مقارنة نص الرسالة، لأن النص قد يختلف بين إصدارات السائق (driver).

إن كان قصدك فعليًا «حدّث إن وُجد، أنشئ إن لم يوجد»، upsert يزيل الحاجة لهذا التحقّق من الأساس:

await db.users.updateOne(
  { email: "user@example.com" },
  { $set: { lastLogin: new Date() } },
  { upsert: true }
);

السبب الثاني: فهرس قديم نسيته بعد تعديل المخطّط

شائع جدًا في مشاريع قيد التطوير: أنشأت فهرسًا unique مبكّرًا، ثم غيّرت رأيك في منطق التطبيق، لكن الفهرس نفسه بقي في قاعدة البيانات — لأن حذف/تعديل الفهارس لا يحدث تلقائيًا عند تعديل كود المخطّط (حتى لو كنت تستخدم Mongoose).

// اعرض كل الفهارس الفعلية على المجموعة أولًا
db.users.getIndexes()

// احذف الفهرس القديم غير المرغوب فيه بعد التأكد
db.users.dropIndex("username_1")

السبب الثالث: تكرار قيمة null أو حقل مفقود

الحالة الأقل توقّعًا: فهرس unique عادي (غير sparse) يعامل الحقل المفقود بالكامل كـ null، ويسمح بمستند واحد فقط بهذه القيمة على كامل المجموعة. أي مستند ثانٍ بلا هذا الحقل يُرفَض بنفس رسالة E11000:

db.users.createIndex({ username: 1 }, { unique: true })

db.users.insertOne({ email: "a@x.com" })  // ✅ ينجح، username مفقود = null
db.users.insertOne({ email: "b@x.com" })  // ❌ E11000 — null مكرَّر

الحل: استخدم فهرسًا sparse إن كان الحقل اختياريًا فعلًا، حتى لا تُدرَج المستندات الخالية منه في الفهرس أصلًا:

db.users.createIndex({ username: 1 }, { unique: true, sparse: true })

جدول تشخيص سريع

العلامةالسبب المرجّحالحل
القيمة نفسها موجودة فعلًا في مستند آخرتكرار بيانات حقيقيتحقّق قبل الإدراج، أو استخدم upsert
الرسالة تذكر فهرسًا لا تتذكّره في الكود الحاليفهرس قديم من مخطّط سابقgetIndexes() ثم dropIndex()
القيمة المكرَّرة في الرسالة هي nullحقل اختياري بفهرس unique عاديأضف sparse: true للفهرس

الخلاصة

E11000 ليست رسالة عطل غامضة — هي فهرس unique يقوم بعمله بالضبط. اقرأ اسم الفهرس والقيمة من نص الرسالة، ثم صنّف السبب بين الثلاثة أعلاه قبل أن تلمس أي كود. وإن كان المنطق المقصود فعليًا "أنشئ أو حدّث"، صمّم الكتابة بـ upsert من البداية بدل معالجة الخطأ لاحقًا.

راجع درس الفهارس في مسار MongoDB لأساسيات الفهرسة، ومرجع دوال MongoDB لصياغة updateOne وfindOneAndUpdate كاملة، وإن كنت لا تزال تقارن بين الخيارات فراجع الفرق بين MongoDB و PostgreSQL.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ MongoDB

الأسئلة الشائعة

لماذا يظهر E11000 رغم أني متأكد أن القيمة غير مكرَّرة؟

الشك الأول: فهرس قديم لم يعد يطابق منطق تطبيقك الحالي، أو تعديل تحقّقت منه فقط في كود التطبيق (مثل Mongoose) بينما القاعدة نفسها ما زالت تحمل فهرسًا unique من إعداد سابق. شغّل db.collection.getIndexes() لترى كل الفهارس الفعلية على المجموعة — أحيانًا الفهرس المسبّب للخطأ لم يعد أحد يتذكّر أنه موجود.

هل يمكن أن يسبّب حقل لم أملأه أصلًا هذا الخطأ؟

نعم — إذا كان هذا الحقل مغطّى بفهرس unique، فـ MongoDB تعامل القيمة المفقودة كـ null، وتسمح بمستند واحد فقط بقيمة null على ذلك الفهرس. أي مستند ثانٍ بلا هذا الحقل يصطدم بنفس الخطأ وكأنه يكرّر قيمة null الموجودة.

كيف أتجنّب هذا الخطأ عمليًا بدل معالجته بعد وقوعه؟

استخدم findOneAndUpdate أو updateOne مع upsert: true عندما يكون قصدك فعليًا «أدرج إن لم يوجد، حدّث إن وُجد» — هذا يزيل الحاجة لتحقّق يدوي منفصل قبل الكتابة، ويحوّل حالة التكرار من خطأ غير متوقَّع إلى سلوك طبيعي متوقَّع في الكود.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.