منع السجلات المكررة في تطبيقات CRUD يتطلب طبقات: قيود فريدة في قاعدة البيانات، مفاتيح idempotency، وحالات واجهة تمنع الإرسال المزدوج.

السجل المكرر هو عندما يخزن تطبيقك نفس الشيء مرتين. قد يكون طلبين لنفس عملية الدفع، تذكرتين دعم بنفس التفاصيل، أو حسابين تم إنشاؤهما من نفس سير التسجيل. في تطبيق CRUD، تبدو التكرارات مثل صفوف عادية بذاتها، لكنها خاطئة عندما تنظر إلى البيانات ككل.
تبدأ معظم التكرارات بسلوك عادي. ينقر شخص ما على إنشاء مرتين لأن الصفحة تبدو بطيئة. على الجوال، النقر المزدوج سهل الوقوع. حتى المستخدمون الحريصون سيحاولون مرة أخرى إذا ظل الزر نشطًا ولم يكن هناك مؤشر واضح بأن شيئًا يحدث.
ثم هناك الوساطة الفوضوية: الشبكات والخوادم. قد ينتهي الطلب بمهلة ويتم إعادة إرساله تلقائيًا. قد يكرر مكتبة العميل طلب POST إذا اعتقدت أن المحاولة الأولى فشلت. قد تنجح المحاولة الأولى لكن تضيع الاستجابة، فيجرب المستخدم مرة أخرى فيُنشأ نسخة ثانية.
لا يمكنك حل هذا بطبقة واحدة فقط لأن كل طبقة ترى جزءًا من القصة. يمكن للواجهة تقليل الإرسالات العرضية المزدوجة، لكنها لا تستطيع إيقاف المحاولات المعادة بسبب اتصالات سيئة. يمكن للخادم اكتشاف التكرارات، لكنه يحتاج إلى طريقة موثوقة للتعرّف على "هذا نفس الإنشاء مرة أخرى". يمكن لقاعدة البيانات فرض قواعد، لكن فقط إذا عرّفت ما يعنيه "نفس الشيء".
الهدف بسيط: اجعل عمليات الإنشاء آمنة حتى لو حدث نفس الطلب مرتين. يجب أن تتحول المحاولة الثانية إلى لا-عمل، استجابة نظيفة "تم إنشاؤها بالفعل"، أو تعارض مُتحكم به، لا صف ثانٍ.
تعامل العديد من الفرق مع التكرارات على أنها مشكلة قاعدة بيانات. في الواقع، تولد التكرارات عادةً في وقت أبكر، عندما يتم تشغيل نفس فعل الإنشاء أكثر من مرة.
ينقر المستخدم على إنشاء ولا يبدو أن شيئًا يحدث، فينقر مرة أخرى. أو يضغط Enter ثم ينقر الزر بعد ذلك. على الجوال، قد تحصل على نقرتين سريعًا، أو تداخل بين أحداث اللمس والنقر، أو إيماءة تُسجل مرتين.
حتى لو أرسل المستخدم مرة واحدة فقط، قد تعيد الشبكة الطلب. قد تؤدي المهلة إلى إعادة المحاولة. قد يُعِد تطبيق غير متصل "حفظ" ويُعيد إرساله عند إعادة الاتصال. بعض مكتبات HTTP تعيد المحاولة تلقائيًا على أخطاء معينة، ولن تلاحظ حتى ترى صفوفًا مكررة.
الخوادم تكرر العمل عمدًا. قوائم المهام تعيد المحاولات للمهام الفاشلة. مزودو webhooks غالبًا ما يرسلون نفس الحدث أكثر من مرة، خصوصًا إذا كان نقطة النهاية بطيئة أو تُعيد حالة ليست 2xx. إذا كان منطق الإنشاء يُشغَّل بواسطة هذه الأحداث، افترض أن التكرارات ستحدث.
التزامن يولد أذكى التكرارات. تبويبان يرسلان نفس النموذج خلال ميليثانية. إذا كان خادمك يفعل "هل هذا موجود؟" ثم يُدرج، يمكن لكلتا الطلبتين أن تتجاوزا الفحص قبل حدوث أي إدراج.
عامل العميل والشبكة والخادم كمصادر منفصلة للتكرار. ستحتاج إلى دفاعات في كل منها.
إذا أردت مكانًا موثوقًا لإيقاف التكرارات، ضع القاعدة في قاعدة البيانات. إصلاحات الواجهة وفحوصات الخادم تساعد، لكنها قد تفشل تحت المحاولات المعادة، التأخر، أو مستخدمين اثنين يعملان في نفس الوقت. قيد فريد في قاعدة البيانات هو السلطة النهائية.
ابدأ باختيار قاعدة فرادة حقيقية تتوافق مع كيفية تفكير الناس في السجل. أمثلة شائعة:
كن حذرًا مع الحقول التي تبدو فريدة لكن ليست كذلك، مثل الاسم الكامل.
بعد أن تحدد القاعدة، فرِضها بقيد فريد (أو فهرس فريد). هذا يجعل قاعدة البيانات ترفض إدراجًا ثانيًا يخرق القاعدة، حتى لو وصلت طلبتان في نفس اللحظة.
عندما يشتغل القيد، قرر ما ينبغي أن يراه المستخدم. إذا كان إنشاء مكرر دائمًا خطأً، امنعه برسالة واضحة ("هذا البريد مستخدم بالفعل"). إذا كانت المحاولات شائعة والسجل موجود بالفعل، غالبًا ما يكون أفضل أن تعامل المحاولة المتكررة على أنها نجاح وتعيد السجل الموجود ("تم إنشاء طلبك بالفعل").
إذا كان هدف إنشاءك فعليًا "إنشاء أو إعادة استخدام"، قد يكون upsert أنظف الحلول. مثال: "إنشاء عميل حسب البريد الإلكتروني" يمكن أن يُدرج صفًا جديدًا أو يعيد الموجود. استعمل ذلك فقط عندما يتطابق مع المعنى التجاري. إذا قد تصل حمولات مختلفة قليلاً لنفس المفتاح، قرر أي الحقول يمكن تحديثها وأيها يجب أن تبقى ثابتة.
لا تحل القيود الفريدة محل مفاتيح idempotency أو حالات واجهة جيدة، لكنها تمنحك إيقافًا صارمًا يمكن لباقي الطبقات الاعتماد عليه.
مفتاح idempotency هو رمز فريد يمثل نية مستخدم واحدة، مثل "إنشاء هذا الطلب مرة واحدة". إذا أُرسل نفس الطلب مرة أخرى (نقرة مزدوجة، إعادة محاولة الشبكة، استئناف الجوال)، يعامل الخادم ذلك كمحاولة متكررة، لا كإنشاء جديد.
هذه واحدة من أكثر الأدوات العملية لجعل نقاط نهاية الإنشاء آمنة عندما لا يستطيع العميل تحديد ما إذا نجحت المحاولة الأولى.
تستفيد نقاط النهاية الأكثر أهمية حيث يكون التكرار مكلفًا أو مربكًا، مثل الطلبات، الفواتير، المدفوعات، الدعوات، الاشتراكات، والنماذج التي تُطلق رسائل بريد إلكتروني أو webhooks.
عند المحاولة المتكررة، يجب أن يعيد الخادم النتيجة الأصلية من المحاولة الناجحة الأولى، بما في ذلك نفس معرف السجل وحالة الاستجابة. لتحقيق ذلك، خزّن سجل idempotency صغير مفتاحه هو (المستخدم أو الحساب) + نقطة النهاية + مفتاح idempotency. احفظ كل من النتيجة (معرف السجل، جسم الاستجابة) وحالة "قيد التنفيذ" حتى لا ينشئ طلبان متقاربان صفين.
احتفظ بسجلات idempotency طولًا كافيًا لتغطية محاولات حقيقية. القاعدة الشائعة هي 24 ساعة. لعمليات الدفع، يحتفظ العديد منها 48-72 ساعة. يحافظ TTL على حد للتخزين ويتطابق مع المدة التي قد يحدث فيها إعادة المحاولة.
إذا أنشأت واجهات برمجة باستخدام منشئ يقوده دردشة مثل Koder.ai، لا تزال تريد جعل idempotency صريحة: اقبل مفتاحًا مرسلاً من العميل (في الهيدر أو كحقل) وفرض "نفس المفتاح، نفس النتيجة" على الخادم.
يجعل idempotency طلب الإنشاء آمنًا للتكرار. إذا أعاد العميل المحاولة بسبب مهلة (أو نقر المستخدم مرتين)، يعيد الخادم نفس النتيجة بدلًا من إنشاء صف ثانٍ.
Idempotency-Key)، لكن إرساله في جسم JSON كذلك ممكن.التفصيل المهم هو أن "التحقق + الحفظ" يجب أن يكون آمنًا تحت التزامن. عمليًا، خزّن سجل idempotency مع قيد فريد على (scope, key) وتعامل مع التضارب كإشارة لإعادة الاستخدام.
if idempotency_record exists for (user_id, key):
return saved_response
else:
begin transaction
create the row
save response under (user_id, key)
commit
return response
مثال: يضغط العميل "إنشاء فاتورة"، يرسل التطبيق المفتاح abc123، ويُنشئ الخادم الفاتورة inv_1007. إذا انقطع الاتصال على الهاتف وأُعيدت المحاولة، يرد الخادم بنفس استجابة inv_1007، لا inv_1008.
عند الاختبار، لا تتوقف عند "نقرة مزدوجة". حفّظ طلبًا ينتهي بمهلة على العميل لكنه يُنجز على الخادم، ثم أعد المحاولة بنفس المفتاح.
تهم الدفاعات على الخادم، لكن العديد من التكرارات تبدأ ببساطة عندما يفعل شخص شيء طبيعي مرتين. واجهة جيدة تجعل المسار الآمن واضحًا.
عطّل زر الإرسال بمجرد تقديم المستخدم. افعل ذلك عند النقر الأول، ليس بعد التحقق أو بعد بدء الطلب. إذا أمكن إرسال النموذج عبر وسائل متعددة (زر وEnter)، أقفل حالة النموذج بأكملها، لا زرًا واحدًا فقط.
أظهر حالة تقدم واضحة تجيب على سؤال واحد: هل يعمل؟ تسمية بسيطة "جارٍ الحفظ..." أو مؤشر دوران تكفي. حافظ على ثبات التصميم حتى لا يقفز الزر ويغري بالنقر مرة أخرى.
مجموعة قواعد صغيرة تمنع معظم الإرسالات المزدوجة: ضع علامة isSubmitting في بداية مُعالج الإرسال، تجاهل الإرسالات الجديدة بينما هي true (للزر وEnter)، ولا تمحوها حتى تحصل على استجابة حقيقية.
الاستجابات البطيئة هي المكان الذي تتسلل منه الكثير من التطبيقات. إذا أعِدت تمكين الزر على مؤقت ثابت (مثلاً بعد ثانيتين)، قد يعيد المستخدم الإرسال بينما الطلب الأول لا يزال في الهواء. أعِد التمكين فقط عند اكتمال المحاولة.
بعد النجاح، اجعل إعادة الإرسال غير مرجحة. انتقل بعيدًا (إلى صفحة السجل الجديد أو القائمة) أو أظهر حالة نجاح واضحة مع السجل المنشأ مرئيًا. تجنب ترك نفس النموذج المعبأ على الشاشة مع زر ممكن الضغط عليه.
تأتي أخطر أخطاء التكرار من سلوكيات يومية "غريبة لكنها شائعة": تبويبان، تحديث الصفحة، أو هاتف يفقد الإشارة.
أولًا، حدِّد نطاق الفرادة بشكل صحيح. "الفريد" نادرًا ما يعني "فريد في كل قاعدة البيانات". قد يعني واحدًا لكل مستخدم، أو لكل مساحة عمل، أو لكل مستأجر. إذا كنت تزامن مع نظام خارجي، قد تحتاج فرادة لكل مصدر خارجي بالإضافة إلى معرفه الخارجي. النهج الآمن هو كتابة الجملة التي تقصدها بالضبط (مثل "رقم فاتورة واحد لكل مستأجر في السنة") ثم فرضها.
سلوك التبويبات المتعددة فخ كلاسيكي. حالات التحميل في تبويب تساعد في تبويب واحد، لكنها لا تفعل شيئًا عبر التبويبات. هنا يجب أن تقف الدفاعات على الخادم.
زر الرجوع والتحديث يمكن أن يسهما في الإرسالات العرضية. بعد إنشاء ناجح، كثير من المستخدمين يحدثون الصفحة لـ"التحقق"، أو يضغطون Back ثم يعيدون إرسال نموذج ما زال قابلاً للتعديل. فضّل عرض نافذة السجل المنشأ بدلًا من النموذج الأصلي، واجعل الخادم يتعامل مع إعادة التشغيل بأمان.
يضيف الجوال انقطاعات: إيقاف التطبيق في الخلفية، شبكات متقلبة، ومحاولات تلقائية. قد ينجح الطلب لكن لا يستلم التطبيق الاستجابة، فيحاول مرة أخرى عند الاستئناف.
أكثر أنماط الفشل شيوعًا هو اعتبار الواجهة خط الحماية الوحيد. زر معطل ومؤشر تحميل يساعدان، لكن لا يغطيان التحديثات، شبكات الجوال المتقلبة، تبويبات متعددة، أو علة في العميل. يجب أن يكون الخادم وقاعدة البيانات قادرين على قول "هذا الإنشاء حدث بالفعل".
فخ آخر هو اختيار الحقل الخطأ للفرادة. إذا فرضت قيدًا فريدًا على شيء ليس فريدًا فعليًا (اسم عائلة، طابع زمني مدور، عنوان حر)، ستحظر سجلات صحيحة. بدلًا من ذلك استخدم معرفًا حقيقيًا (مثل معرف مزود خارجي) أو قاعدة مُقَيَّدة (فريد لكل مستخدم، لكل يوم، أو لكل سجل أب).
مفاتيح idempotency أيضًا سهلة التنفيذ بشكل خاطئ. إذا أنشأ العميل مفتاحًا جديدًا عند كل إعادة محاولة، ستحصل على إنشاء جديد في كل مرة. احتفظ بنفس المفتاح طوال نية المستخدم، من النقر الأول وحتى أي محاولات إعادة.
راقب أيضًا ما تعيده عند المحاولات. إذا أنشأ الطلب الأول السجل، يجب أن تعيد المحاولة نفس النتيجة (أو على الأقل نفس معرف السجل)، لا خطأ غامض يدفع المستخدم للمحاولة مجددًا.
إذا حجب قيد فريد تكرارًا، لا تخفِ ذلك خلف "حدث خطأ ما". قل ما حدث بلغة بسيطة: "رقم الفاتورة هذا موجود بالفعل. احتفظنا بالأصل ولم ننشئ صفًا ثانيًا."
قبل الإصدار، قم بمراجعة سريعة لمسارات الإنشاء المكررة. أفضل النتائج تأتي من تكديس الدفاعات حتى نقرة فائتة، إعادة محاولة، أو شبكة بطيئة لا تستطيع إنشاء صفين.
أكد ثلاثة أشياء:
فحصٌ عملي بسيط: افتح النموذج، انقر إرسال مرتين بسرعة، ثم حدّث الصفحة أثناء الإرسال وجرب مرة أخرى. إذا استطعت إنشاء سجلين، فالمستخدمون الحقيقيون سيفعلون ذلك أيضًا.
تخيل تطبيق فواتير صغير. يملأ المستخدم فاتورة جديدة وينقر Create. الشبكة بطيئة، الشاشة لا تتغير فورًا، فينقر Create مرة أخرى.
مع حماية الواجهة فقط، قد تعطل الزر وتعرض مؤشر تحميل. هذا يساعد، لكنه قد لا يكفي. قد يمر نقر مزدوج على بعض الأجهزة، قد تحدث إعادة محاولة بعد مهلة، أو قد يرسل المستخدم من تبويبان.
مع قيد فريد فقط في قاعدة البيانات، يمكنك إيقاف التكرارات الدقيقة، لكن التجربة قد تكون صعبة. تنجح المحاولة الأولى، تضرب المحاولة الثانية القيد، ويرى المستخدم خطأ رغم أن الفاتورة تُنشأ.
النتيجة النظيفة هي idempotency زائد قيد فريد:
رسالة واجهة بسيطة بعد النقر الثاني: "تم إنشاء الفاتورة - تجاهلنا الإرسال المكرر واحتفظنا بطلبك الأول."
بعد وضع الأساس، الإنجازات التالية حول الرؤية، التنظيف، والاتساق.
أضف تسجيلًا خفيفًا حول مسارات الإنشاء حتى تعرف الفرق بين فعل مستخدم حقيقي ومحاولة مكررة. سجِّل مفتاح idempotency، الحقول الفريدة المعنية، والنتيجة (تم الإنشاء مقابل إعادة الموجود مقابل الرفض). لا تحتاج إلى أدوات ثقيلة لتبدأ.
إذا كانت التكرارات موجودة بالفعل، نظّفها بقواعد واضحة وسجل تدقيق. على سبيل المثال، احتفظ بالأقدم كـ"الرابح"، أعد ربط الصفوف المرتبطة (مدفوعات، بنود)، وعَلِّم الآخرين كـmerged بدلًا من حذفها. هذا يسهل الدعم والتقارير.
اكتب قواعد الفرادة وidempotency في مكان واحد: ما هو الفريد وفي أي نطاق، كم يعيش مفتاح idempotency، كيف تبدو الأخطاء، وماذا يجب أن تفعل الواجهة عند المحاولات. هذا يمنع نقاط نهاية جديدة من تجاوز وسائل الأمان بصمت.
إذا كنت تبني شاشات CRUD بسرعة في Koder.ai (koder.ai)، فاجعل هذه السلوكيات جزءًا من قالبك الافتراضي: قيود فريدة في المخطط، نقاط إنشاء idempotent في API، وحالات تحميل واضحة في الواجهة. بهذه الطريقة، لا تأتي السرعة بثمن بيانات فوضوية.
السجل المكرر هو عندما يتم تخزين نفس الشيء الحقيقي مرتين، مثل طلبين لنفس عملية الدفع أو تذكرتين لمشكلة واحدة. عادة يحدث ذلك لأن نفس عملية “الإنشاء” تشتغِل أكثر من مرة بسبب نقر المستخدم المزدوج، محاولات الإرسال التلقائية، أو طلبات متزامنة.
لأنه قد يحدث إنشاء ثانٍ دون أن يدرك المستخدم ذلك، مثل نقرة مزدوجة على الهاتف أو الضغط على Enter ثم النقر على الزر. حتى لو أرسل المستخدم مرة واحدة، قد يعيد العميل أو الشبكة أو الخادم الطلب بعد مهلة، ولا يمكن للخادم افتراض أن "POST يعني مرة واحدة".
ليس بشكل موثوق. تعطيل الزر وإظهار "جاري الحفظ..." يقللان من النقرات العرضية المزدوجة، لكنهما لا يمنعان المحاولات المتكررة نتيجة شبكات غير مستقرة، التحديث/الريفرش، علامات تبويب متعددة، عمال الخلفية، أو إعادة إرسال الويبهوك. تحتاج أيضًا إلى دفاعات على الخادم وقاعدة البيانات.
القيود الفريدة في قاعدة البيانات هي الخط الأخير الذي يمنع إدخال صفين حتى لو وصلت طلبتان في نفس اللحظة. تعمل بشكل أفضل عندما تعرف قاعدة فرادة حقيقية (غالبًا ما تكون مُقَيَّدة، مثل لكل مستأجر أو لكل مساحة عمل) وتفرضها مباشرة في قاعدة البيانات.
كلاهما يعالجان مشكلات مختلفة. القيود الفريدة تمنع التكرارات بناءً على حقل (مثل رقم الفاتورة)، بينما مفاتيح idempotency تجعل محاولة إنشاء محددة آمنة للتكرار (نفس المفتاح يعيد نفس النتيجة). استخدامهما معًا يمنحك أمانًا وتجربة مستخدم أفضل عند التعافي من المحاولات.
أنشئ مفتاحًا واحدًا لكل نية مستخدم (مثل نقرة "إنشاء" واحدة)، أعد استخدامه لكل محاولات إعادة نفس النية، وأرسله مع الطلب في كل مرة. يجب أن يبقى المفتاح ثابتًا عبر مهلات الشبكة واستئناف التطبيق، لكنه لا يُعاد استخدامه لإنشاء آخر لاحقًا.
خزن سجل idempotency مُفَصَّل بنطاق واضح (مثل المستخدم أو الحساب)، ونقطة النهاية، والمفتاح نفسه، واحفظ الاستجابة التي أعدتها للطلب الناجح الأول. إذا وصل نفس المفتاح مرة أخرى، أعد الاستجابة المحفوظة مع نفس معرف السجل المنشأ بدلًا من إنشاء صف جديد.
استخدم نهج "تحقق + احفظ" آمن للتزامن، عادة بفرض قيد فريد على سجل idempotency نفسه (بالنطاق والمفتاح). بهذه الطريقة لا يمكن لطلبين متقاربين أن يدّعيا أنهما الأول، وإحداهما ستُجبَر على إعادة استخدام النتيجة المخزنة.
احتفظ بها لفترة تكفي لتغطية المحاولات الواقعية؛ الافتراضي الشائع حوالي 24 ساعة، وأكثر في تدفقات مثل المدفوعات حيث قد تحدث المحاولات لاحقًا. أضف TTL حتى لا ينمو التخزين بلا حدود، واجعل مدة TTL مطابقة للمدة التي قد يعيد فيها العميل المحاولة.
عامل إنشاء مكرر كمحاولة ناجحة عند تأكد أنه نفس النية، وأعد السجل المنشأ الأصلي (بنفس الـ ID) بدلًا من خطأ غامض. إذا كان العنصر يجب أن يكون فريدًا فعليًا (مثل بريد إلكتروني)، أعِد رسالة صراع واضحة تشرح الموجود وما الذي حدث بعد ذلك.