20 أغسطس 2025·7 دقيقة قراءة

حل التعارضات في النماذج بدون اتصال لـ Kotlin + SQLite

تعلم حل تعارضات النماذج دون اتصال: قواعد دمج واضحة، تدفق مزامنة بسيط لـ Kotlin + SQLite، وأنماط UX عملية لتصادمات التحرير.

حل التعارضات في النماذج بدون اتصال لـ Kotlin + SQLite

ماذا يحدث فعلاً عندما يحرر اثنان نفس السجل دون اتصال

نماذج "أوفلاين-أولًا" تتيح للناس عرض وتعديل البيانات حتى عندما تكون الشبكة بطيئة أو غير متاحة. بدلاً من انتظار الخادم، يكتب التطبيق التغييرات أولًا إلى قاعدة SQLite محلية ثم يزامنها لاحقًا.

هذا يعطي إحساسًا بالسرعة، لكنه يخلق واقعًا بسيطًا: جهازان يمكن أن يغيرا نفس السجل دون أن يعرف أحدهما عن الآخر.

مثال شائع للتعارض: فني ميدان يفتح أمر عمل على جهاز لوحي في قبو بلا إشارة. يعلّم الحالة على Done ويضيف ملاحظة. في نفس الوقت، مشرف على هاتف آخر يحدّث نفس أمر العمل، يعيد تعيينه ويغير تاريخ الاستحقاق. كلاهما يضغط حفظ. كلا الحفظين نجحا محليًا. لا أحد أخطأ.

عندما يحدث التزامن، يجب على الخادم أن يقرر ما هو السجل "الحقيقي". إذا لم تتعامل مع التعارضات صراحة، فعادة تحصل على أحد هذه النتائج:

  • آخر كاتب يفوز: المزامنة الأحدث تستبدل التغييرات القديمة وفقدان بيانات يحصل.
  • فشل صريح: المزامنة ترفض تحديثًا ويعرض التطبيق خطأ غير مفيد.
  • سجلات مكررة: النظام ينشئ نسخة ثانية لتفادي الاستبدال وتصبح التقارير فوضوية.
  • دمج صامت: النظام يجمع التغييرات لكنه يركّب الحقول بطريقة لا يتوقعها المستخدمون.

التعارضات ليست عيبًا. إنها نتيجة متوقعة لمنح الناس القدرة على العمل دون اتصال حي، وهذا هو هدف "أوفلاين-أولًا".

الهدف ذو شقين: حماية البيانات والحفاظ على سهولة استخدام التطبيق. عادة يعني هذا قواعد دمج واضحة (غالبًا على مستوى الحقل) وتجربة مستخدم تقاطع الناس فقط عندما يكون الأمر مهمًا فعلاً. إذا لمست التعديلات حقولًا مختلفة، يمكنك غالبًا الدمج بهدوء. إذا غيّر اثنان نفس الحقل بطرق مختلفة، يجب أن يجعل التطبيق ذلك مرئيًا ويساعد شخصًا ما على اختيار النتيجة الصحيحة.

اختر استراتيجية تعارض تتناسب مع بياناتك

التعارضات ليست مشكلة تقنية في المقام الأول. هي قرار منتج حول ما يعنيه "الصحيح" عندما يغيّر شخصان نفس السجل قبل المزامنة.

ثلاث استراتيجيات تغطي معظم تطبيقات الأوفلاين:

  • آخر كاتب يفوز (LWW): قبول التعديل الأحدث واستبدال الأقدم.
  • المراجعة اليدوية: التوقف وطلب قرار بشري حول ما يحفظ.
  • الدمج على مستوى الحقل: دمج التغييرات لكل حقل وطلب القرار فقط عندما لمسها كلا الطرفين.

LWW مقبول عندما تكون السرعة أهم من الدقة الكاملة وتكلفة الخطأ منخفضة. فكّر بالملاحظات الداخلية، الوسوم غير الحرجة، أو حالة مسودة يمكن تعديلها لاحقًا.

المراجعة اليدوية هي الخيار الأكثر أمانًا للحقول ذات التأثير العالي حيث لا ينبغي للتطبيق التخمين: نصوص قانونية، تأكيدات امتثال، مبالغ الرواتب والفواتير، تفاصيل بنكية، تعليمات دواء، وكل ما قد يخلق مسؤولية.

الدمج على مستوى الحقل هو غالبًا الأفضل افتراضيًا للنماذج التي تحدث فيها أدوار مختلفة أجزاء مختلفة. وكيل دعم يحدّث العنوان بينما المبيعات تحدث تاريخ التجديد. دمج كل حقل يحتفظ بكلتا التغييرات دون إزعاج أحد. لكن إذا حرر الاثنان تاريخ التجديد، يجب أن يثير ذلك قرارًا لذلك الحقل.

قبل التنفيذ، دوّن ماذا يعني "الصحيح" لعملك. قائمة تحقق سريعة تساعد:

  • أي الحقول يجب أن تعكس دائمًا القيمة الحقيقية الأخيرة (مثل الحالة الحالية)؟
  • أي الحقول تاريخية ولا يجب أن تُستبدل (مثل وقت الإرسال)؟
  • من مسموح له تغيير كل حقل (دور، ملكية، موافقات)؟
  • ما مصدر الحقيقة عند اختلاف القيم (الجهاز، الخادم، موافقة المدير)؟
  • ماذا يحدث إن اخترت خطأ (إزعاج بسيط أم أثر مالي/قانوني)؟

عندما تصبح هذه القواعد واضحة، يكون لمزامنة الكود وظيفة واحدة: تنفيذها.

عرّف قواعد دمج لكل حقل، لا لكل شاشة

عندما يحدث تعارض، نادرًا ما يؤثر على النموذج ككل بالتساوي. قد يحدث أن يحدّث مستخدم رقم هاتف بينما يضيف آخر ملاحظة. إذا عاملت السجل كله ككل أو لا شيء، تُجبر الناس على إعادة العمل الجيد.

الدمج على مستوى الحقل أكثر توقعًا لأن لكل حقل سلوك معروف. تظل تجربة المستخدم هادئة وسريعة.

طريقة بسيطة للبدء هي تقسيم الحقول إلى فئتين: "آمنة عادة" و"غير آمنة عادة" للدمج تلقائيًا.

آمنة عادة للدمج تلقائيًا: الملاحظات والتعليقات الداخلية، الوسوم، المرفقات (غالبًا اتحاد)، والطوابع الزمنية مثل last contacted (غالبًا نحتفظ بالأحدث).

غير آمنة عادة للدمج تلقائيًا: الحالة/state، assignee/المالك، الإجماليات/الأسعار، أعلام الموافقة، وأعداد المخزون.

ثم اختر قاعدة أولوية لكل حقل. الخيارات الشائعة: الخادم يفوز، العميل يفوز، الدور يفوز (مثلاً المدير يتجاوز الوكيل)، أو كاسر تعادل حتمي مثل أحدث إصدار على الخادم.

السؤال الأساسي: ماذا يحدث عندما غيّر الطرفان نفس الحقل؟ لكل حقل اختر سلوكًا واحدًا:

  • دمج تلقائي مع قاعدة واضحة (مثلاً الوسوم هي اتحاد)
  • الاحتفاظ بالقيمتين (مثلاً إلحاق الملاحظات مع ذكر الكاتب والوقت)
  • وسم للمراجعة (مثلاً الحالة والمكلّف تتطلب اختيار)

مثال: محرّكا دعم حررا نفس التذكرة دون اتصال. الممثل A غيّر status من Open إلى Pending. الممثل B غيّر notes وأضاف وسم refund. عند المزامنة، يمكنك دمج notes وtags بأمان، لكن لا ينبغي دمج status بصمت. اطلب القرار فقط لحقل status، مع دمج كل شيء آخر.

لتفادي الخلاف لاحقًا، وثّق كل قاعدة بعبارة واحدة لكل حقل:

  • notes: احتفظ بكلا القيمتين، ألصق الأحدث أخيرًا، أدرج الكاتب والوقت.
  • tags: اتحاد، احذف فقط إذا أُزيل صراحة على كلا الجانبين.
  • status: إذا تغيّر على كلا الجانبين، اطلب اختيار المستخدم.
  • assignee: المدير يفوز، وإلا الخادم يفوز.

تصبح تلك الجملة مصدر الحقيقة لكود Kotlin واستعلامات SQLite وواجهة حل التعارض.

أساسيات نموذج البيانات: الإصدارات وحقول التدقيق في SQLite

إذا أردت أن تبدو التعارضات متوقعة، أضف مجموعة صغيرة من أعمدة البيانات الوصفية لكل جدول متزامن. بدونها، لا يمكنك معرفة إن كنت تنظر إلى تعديل جديد، أو نسخة قديمة، أو تعديلين يحتاجان دمجًا.

الحد العملي لكل سجل متزامن على الخادم:

  • id (مفتاح رئيسي ثابت): لا تعيد استخدامه
  • version (عدد صحيح): يزيد مع كل كتابة ناجحة على الخادم
  • updated_at (طابع زمني): وقت آخر تغيير للسجل
  • updated_by (نص أو معرف مستخدم): من قام بالتغيير الأخير

على الجهاز، أضف حقولًا محلية لتتبع التغييرات غير المؤكدة من الخادم:

  • dirty (0/1): توجد تغييرات محلية
  • pending_sync (0/1): في قائمة الرفع لكن غير مؤكدة
  • last_synced_at (طابع زمني): آخر مرة تطابق فيها هذه الصف مع الخادم
  • sync_error (نص، اختياري): آخر سبب فشل للعرض في واجهة المستخدم

التزامن التفاؤلي هو القاعدة الأبسط التي تمنع الاستبدالات الصامتة: كل تحديث يتضمن الإصدار الذي تظن أنك تعدله (expected_version). إذا ظل سجل الخادم على ذلك الإصدار، يُقبل التحديث ويعيد الخادم الإصدار الجديد. إن لم يكن، فهو تعارض.

مثال: المستخدم A وB حملا version = 7. A يزامن أولًا؛ الخادم يزيد إلى 8. عندما يحاول B المزامنة مع expected_version = 7، يرفض الخادم بوجود تعارض فتقوم تطبيق B بالدمج بدل الاستبدال.

لشاشة حل تعارض جيدة، خزّن نقطة البداية المشتركة: ما الذي بدأ المستخدم التحرير منه. نهجان شائعان:

  • خزّن لقطة من آخر سجل متزامن (عمود JSON واحد أو جدول موازي).
  • خزّن سجل تغييرات (صف لكل تعديل أو حقل لكل تعديل).

اللقطات أبسط وغالبًا كافية للنماذج. سجلات التغيير أثقل ولكنها تشرح بالضبط ما الذي تغيّر حقلًا بحقل.

بأي حال، يجب أن تستطيع واجهة المستخدم إظهار ثلاث قيم لكل حقل: تعديل المستخدم، القيمة الحالية على الخادم، ونقطة البداية المشتركة.

لقطات السجل مقابل سجلات التغييرات: اختر نهجًا

بناء كامل المكدس في مكان واحد
انشئ باك-إند، واجهات ويب وموبايل جاهزة للإنتاج من بيئة no-code واحدة.
جرّب الآن

عند تزامن نماذج أوفلاين، يمكنك رفع السجل الكامل (لقطة) أو رفع قائمة عمليات (سجل تغييرات). كلاهما يعمل مع Kotlin وSQLite، لكنهما يتصرفان بشكل مختلف عندما يحرر شخصان نفس السجل.

الخيار A: لقطات السجل الكاملة

مع اللقطات، كل حفظ يكتب الحالة الكاملة الأخيرة (كل الحقول). عند المزامنة، ترسل السجل مع رقم الإصدار. إن رأى الخادم أن الإصدار قديم، تحصل على تعارض.

هذا بسيط للبناء وسريع للقراءة، لكنه غالبًا يصنع تعارضات أكبر من اللازم. إذا حرّر A رقم الهاتف بينما حرّر B العنوان، يمكن لنهج اللقطات اعتباره اصطدامًا واحدًا كبيرًا رغم أن التعديلات لا تتداخل.

الخيار B: سجلات التغييرات (العمليات)

في سجلات التغييرات، تخزن ما تغيّر وليس السجل كاملًا. كل تعديل محلي يصبح عملية يمكنك إعادة تشغيلها فوق أحدث حالة على الخادم.

العمليات التي غالبًا تُدمج بسهولة:

  • تعيين قيمة حقل (تعيين email لقيمة جديدة)
  • إلحاق ملاحظة (إضافة عنصر ملاحظة جديد)
  • إضافة وسم (إضافة وسم لمجموعة)
  • إزالة وسم (إزالة وسم من مجموعة)
  • تعليم خانة اختيار كمكتملة (تعيين isDone صح مع طابع زمني)

سجلات العمليات تقلل التعارضات لأن كثيرًا من الأفعال لا تتداخل. إلحاق الملاحظات نادرًا ما يتعارض مع إلحاق ملاحظة مختلفة. إضافات/إزالات الوسوم يمكن دمجها كعمليات على المجموعات. للحقول ذات قيمة واحدة، ما تزال بحاجة لقواعد لكل حقل عندما تتنافس تعديلات مختلفة.

المقابل هو التعقيد: تحتاج إلى معرفات عمليات مستقرة، ترتيب (تسلسل محلي ووقت الخادم)، وقواعد للعمليات التي لا تتبادل.

التنظيف: الضغط بعد المزامنة الناجحة

سجلات العمليات تنمو، لذا خطط لكيفية تقليصها.

نهج شائع هو ضغط لكل سجل: بعد أن يتم تأكيد كل العمليات حتى إصدار خادم معلوم، دمجها في لقطة جديدة ثم حذف تلك العمليات الأقدم. احتفظ بذيل قصير فقط إذا احتجت للتراجع، أو للتدقيق، أو للتصحيح السهل.

تدفق تزامن خطوة بخطوة لـ Kotlin + SQLite

منع الاستبدال الصامت
اضبط تحديثات على شكل تصحيحات وفحوصات إصدار حتى لا تُستبدل البيانات الأحدث بصمت.
ابدأ البناء

استراتيجية مزامنة جيدة تدور حول الصرامة فيما ترسل وما تقبله. الهدف بسيط: لا تستبدل بيانات أحدث عن طريق الخطأ، واجعل التعارضات واضحة عندما لا يمكن الدمج بأمان.

تدفّق عملي:

  1. اكتب كل تعديل في SQLite أولًا. احفظ التغييرات في معاملة محلية وضع علامة pending_sync = 1. خزّن local_updated_at وآخر server_version المعروف.

  2. أرسل تصحيحًا، لا السجل كاملًا. عند استعادة الاتصال، أرسل معرف السجل بالإضافة إلى الحقول التي تغيّرت فقط، مع expected_version.

  3. اترك الخادم يرفض الإصدارات غير المتطابقة. إذا لم يتطابق إصدار الخادم مع expected_version، يعيد الخادم حمولة تعارض (سجل الخادم، التغييرات المقترحة، وأي الحقول تختلف). إن تطابقت الإصدارات، يطبق التصحيح، يزيد الإصدار، ويعيد السجل المحدّث.

  4. طبّق الدمج التلقائي أولًا، ثم اسأل المستخدم. نفّذ قواعد دمج على مستوى الحقل. عامل الحقول الآمنة مثل notes مختلفة عن الحقول الحساسة مثل الحالة أو السعر أو assignee.

  5. التزم بالنتيجة النهائية وامسح علامات الانتظار. سواء دمجت تلقائيًا أو حلّ المستخدم التعارض يدويًا، اكتب السجل النهائي إلى SQLite، حدّث server_version، اضبط pending_sync = 0، وسجل بيانات تدقيق كافية لتفسير ما حدث لاحقًا.

مثال: اثنان من مندوبَي المبيعات يحرران نفس الطلب دون اتصال. المندوب A يغير تاريخ التسليم. المندوب B يغير رقم هاتف الزبون. مع التصحيحات، يمكن للخادم قبول كلا التغييرين بسلاسة. إن غيّرا تاريخ التسليم معًا، تُظهر واجهة الاختيار واحدًا واضحًا بدل إجبار إعادة إدخال كاملة.

حافظ على وعد واجهة المستخدم ثابتًا: Saved يعني محفوظًا محليًا. Synced يجب أن تكون حالة منفصلة وواضحة.

أنماط UX لحل التعارضات في النماذج

يجب أن تكون التعارضات استثناء، لا سير العمل الطبيعي. ابدأ بدمج ما هو آمن تلقائيًا، ثم اسأل المستخدم فقط عندما يكون القرار ضروريًا فعلاً.

اجعل التعارضات نادرة بإفتراضات آمنة

إذا حرّر شخصان حقولًا مختلفة، ادمج دون عرض نافذة منبثقة. احتفظ بكلتا التغييرات وأظهر رسالة صغيرة "تم التحديث بعد المزامنة".

اخصص المطالبات فقط للتصادمات الحقيقية: تغيير نفس الحقل على الجهازين، أو تغيير يعتمد على حقل آخر (مثل الحالة مع سبب الحالة).

عندما يجب السؤال، اجعله سريعًا للإنهاء

شاشة التعارض يجب أن تجيب عن شيئين: ما الذي تغيّر، وماذا سيحفظ. قارن القيم جنبًا إلى جنب: "تعديلي"، "تعديلهم"، و"النتيجة المحفوظة". إذا تعارض حقلان فقط، لا تعرض النموذج بأكمله. انتقل مباشرة إلى تلك الحقول واجعل الباقي قراءة فقط.

قلل الإجراءات إلى ما يحتاجه الناس فعلاً:

  • احتفظ بنسختي
  • احتفظ بنسختهم
  • حرّر النتيجة النهائية
  • راجع حقلًا بحقل (فقط عند الحاجة)

الدمج الجزئي هو نقطة فوضى في UX. ظلل فقط الحقول المتعارضة ووسم المصدر بوضوح (Yours وTheirs). اختر الخيار الأكثر أمانًا افتراضيًا حتى يقدر المستخدم التأكيد والمضي قدمًا.

وضّح التوقعات حتى لا يشعر المستخدم بالحبس. اخبرهم بما يحدث لو غادروا: مثلاً، "سنحتفظ بنسختك محليًا ونعيد المحاولة لاحقًا" أو "هذا السجل سيبقى في حالة بحاجة للمراجعة حتى تختار". اجعل تلك الحالة مرئية في القوائم حتى لا تضيع التعارضات.

إذا تبني هذا التدفق في AppMaster، ينطبق نفس النهج: ادمج الحقول الآمنة أولًا، ثم عرض خطوة مراجعة مركزة فقط عندما تتصادم حقول محددة.

حالات معقدة: الحذف، التكرارات، والسجلات المفقودة

بناء تطبيقك بدون اتصال أولاً
بناء مسار نماذج بدون اتصال مع حالات تزامن واضحة ومعالجة التعارضات في مشروع واحد.
جرّب AppMaster

معظم مشكلات المزامنة التي تبدو عشوائية تأتي من ثلاث حالات: حذف شخص ما بينما عدّل آخر، جهازان ينشئان الشيء "نفسه" دون اتصال، أو سجل يختفي ثم يعاد إنشاؤه. هذه تحتاج قواعد صريحة لأن LWW يخيب الآمال غالبًا.

الحذف مقابل التعديل: من ينتصر؟

قرّر إن كان الحذف أقوى من التعديل. في كثير من تطبيقات الأعمال، الحذف ينتصر لأن المستخدمين يتوقعون بقاء العنصر المحذوف محذوفًا.

قواعد عملية:

  • إذا حُذف السجل على أي جهاز، عاملَه كمحذوف في كل مكان، حتى لو كانت هناك تعديلات لاحقة.
  • إن كان الحذف يجب أن يكون قابلًا للاسترجاع، حوّل الحذف إلى حالة مؤرشفة بدل حذف صلب.
  • إن وصل تعديل لسجل محذوف، احتفظ بالتعديل في السجل للتدقيق، لكن لا تستعد السجل للظهور.

تصادمات الإنشاء دون اتصال ومسودات مكررة

تولد النماذج دون اتصال عادة معرفات مؤقتة (مثل UUID) قبل أن يخصص الخادم معرفًا نهائيًا. تحدث التكرارات عندما ينشئ المستخدمون مسودتين لنفس الشيء الواقعي. إن كان لديك مفتاح طبيعي ثابت (رقم إيصال، باركود، بريد إلكتروني مع تاريخ)، استخدمه لاكتشاف التصادمات. إن لم يكن، اقبل أن التكرارات ستحدث وقدّم خيار دمج بسيط لاحقًا.

نصيحة تنفيذية: خزّن كلًا من local_id وserver_id في SQLite. عندما يرد الخادم، اكتب تعيينًا واحتفظ به حتى تتأكد أن أي تغييرات قائمة لا تزال تشير إلى المعرف المحلي.

منع الإنعاش بعد المزامنة

الإنعاش يحدث عندما يحذف الجهاز A سجلًا، لكن الجهاز B كان غير متصل ورفع نسخة قديمة كـ upsert فأعاد إنشاؤه.

الحل هو التومبستون. بدل إزالة الصف فورًا، علّمَه بالحذف عبر deleted_at (وغالبًا deleted_by وdelete_version). خلال المزامنة، عامل التومبستونات كتغييرات حقيقية يمكنها تجاوز الحالات الأقدم غير المحذوفة.

قرّر مدة الاحتفاظ بالتومبستونات. إن كان المستخدمون قد يكونون دون اتصال لأسابيع، احتفظ بها أطول من ذلك. نظفها فقط بعد أن تطمئن أن الأجهزة النشطة تزامنت بعد حذف السجل.

إذا دعمت التراجع، عامل التراجع كتغيير آخر: امسح deleted_at وزد الإصدار.

أخطاء شائعة تؤدي لفقدان بيانات أو إحباط المستخدم

اختبر التزامن بالطريقة الصحيحة
بناء إعداد اختبار تزامن متكرر بجهازين حتى تظل نتائج الدمج قابلة للتنبؤ مع التكرار.
ابدأ البناء

تأتي كثير من إخفاقات المزامنة من افتراضات صغيرة تستبدل بيانات جيدة بصمت.

خطأ 1: الثقة بوقت الجهاز لترتيب التعديلات

الهواتف يمكن أن تكون ساعاتها خاطئة، المناطق الزمنية تتغير، والمستخدمون يغيرون الوقت يدويًا. إذا رتبت التغييرات حسب طوابع أجهزة، ستطبق التعديلات في ترتيب خاطئ يومًا ما.

فضل إصدارات صادرة من الخادم (serverVersion) واعتبر طوابع العميل للعرض فقط. إن اضطُررت لاستخدام الوقت، أضف ضوابط وصالح على الخادم.

خطأ 2: LWW عرضي على الحقول الحساسة

LWW يبدو بسيطًا حتى يصطدم بالحقول التي لا ينبغي أن "يفوز" بها متزامن لاحق. الحالات، الإجماليات، الموافقات، والتعيينات عادة تحتاج قواعد صريحة أو مراجعة يدوية.

قائمة أمان للحقول عالية المخاطر:

  • عامل انتقالات الحالة كآلة حالات، لا كحقل نصي حر.
  • أعد حساب الإجماليات من البنود. لا تدمج الإجماليات كأرقام خام.
  • للعدادات، ادمج بتطبيق فروق بدل اختيار فائز.
  • للملكية أو المعيّن، اطلب تأكيدًا صريحًا عند التعارض.

خطأ 3: استبدال قيم الخادم الأحدث ببيانات مخزنة قديمة

يحدث هذا عندما يعدّل العميل لقطة قديمة ثم يرفع السجل كاملًا. يقبل الخادم فيستبدل تغييرات أحدث.

صحح شكل ما ترسله: أرسل الحقول المتغيرة فقط (أو سجل تغييرات)، بالإضافة إلى إصدار الأساس الذي عدّلته. إذا كان إصدار الأساس متأخرًا، يرفض الخادم أو يفرض الدمج.

خطأ 4: عدم وجود تاريخ من الذي غيّر ماذا

عندما تحدث التعارضات، يريد المستخدم إجابة واحدة: ماذا غيرت؟ وماذا غير الآخر؟ بدون هوية المحرر وتاريخ تغيّر الحقول، تصبح شاشة التعارض تخمينًا.

اخزن updatedBy ووقت التحديث على الخادم، وعلى الأقل مسار تدقيق بسيط لكل حقل.

خطأ 5: واجهة تعارض تجبر مقارنة السجل كاملًا

إجبار الناس على مقارنة سجلات كاملة مرهق. معظم التعارضات حقل واحد إلى ثلاثة حقول. اعرض الحقول المتعارضة فقط، اختر الخيار الأكثر أمانًا مسبقًا، ودع المستخدم يقبل الباقي تلقائيًا.

إذا تبني نماذج في أداة no-code مثل AppMaster، اسعَ لنفس النتيجة: حل التعارضات على مستوى الحقل حتى يتخذ المستخدم قرارًا واضحًا واحدًا بدلاً من التمرير عبر النموذج كله.

قائمة تحقق سريعة وخطوات تالية

إذا أردت أن تبدو التعديلات دون اتصال آمنة، عامل التعارضات كحالة عادية، لا كخطأ. أفضل النتائج تأتي من قواعد واضحة، اختبارات قابلة للتكرار، وتجربة تشرح ما حدث بلغة بسيطة.

قبل إضافة مزيد من الميزات، تأكد من تثبيت هذه الأساسيات:

  • لكل نوع سجل، عيّن قاعدة دمج لكل حقل (LWW، احتفظ بالحد الأقصى/الحد الأدنى، إلحاق، اتحاد، أو اسأل دومًا).
  • خزّن إصدارًا يسيطر عليه الخادم بالإضافة إلى updated_at تسيطر عليه وتحقق منها أثناء المزامنة.
  • شغّل اختبار جهازين حيث يحرر كلاهما نفس السجل دون اتصال، ثم زامن بالترتيبات المختلفة (A ثم B، B ثم A). يجب أن تكون النتيجة متوقعة.
  • اختبر التعارضات الصعبة: حذف مقابل تعديل، وتعديل مقابل تعديل على حقول مختلفة.
  • اجعل الحالة واضحة: اعرض Synced، Pending upload، وNeeds review.

نمذج التدفق الكامل بنموذج حقيقي واحد، لا شاشة عرض توضيحية. استخدم سيناريو واقعي: فني ميدان يحدث ملاحظة عمل على هاتف بينما المرسل يعدل عنوان العمل على جهاز لوحي. إذا لمسوا حقولًا مختلفة، ادمج تلقائيًا وأظهر تلميحًا صغيرًا "تم التحديث من جهاز آخر". إن لمسوا نفس الحقل، وجههم إلى شاشة مراجعة بسيطة مع خيارين ومعاينة واضحة.

عندما تكون جاهزًا لبناء التطبيق المحمول الكامل وواجهات الـ backend معًا، AppMaster (appmaster.io) يمكن أن يساعد. يمكنك نمذجة البيانات، تعريف منطق العمل، وبناء واجهات ويب وموبايل أصلية في مكان واحد، ثم النشر أو تصدير الشفرة المصدرية عندما تصبح قواعد التزامنك ثابتة.

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

ما هو تعريف تعارض التزامن دون اتصال بكلمات بسيطة؟

يصير التعارض عندما يغير جهازان نفس السجل المدعوم من الخادم أثناء العمل دون اتصال (أو قبل أن يتم مزامنتهما)، ثم يرى الخادم أن التحديثين مبنيان على إصدار أقدم. عندها يجب على النظام أن يقرر القيمة النهائية لكل حقل اختلف.

أي استراتيجية تعارض أختار: آخر كاتب يفوز، مراجعة يدوية، أم دمج على مستوى الحقل؟

ابدأ بـ الدمج على مستوى الحقل كخيار افتراضي لمعظم نماذج الأعمال، لأن أدوارًا مختلفة غالبًا ما تعدل حقولًا مختلفة ويمكنك الاحتفاظ بكلتا التغييرات دون إزعاج المستخدمين. استخدم المراجعة اليدوية فقط للحقول التي قد تسبب ضررًا حقيقيًا إذا خمّنت بشكل خاطئ (أموال، موافقات، امتثال). استخدم آخر كاتب يفوز فقط للحقول منخفضة المخاطر حيث قبول فقدان تعديل أقدم مقبول.

متى يجب أن يطلب التطبيق من المستخدم حل التعارض؟

إذا لمسّا تحريران حقولًا مختلفة، يمكنك عادة الدمج تلقائيًا وإبقاء واجهة المستخدم هادئة. إذا غيّرا نفس الحقل إلى قيم مختلفة، فيجب أن يُثار قرار لهذا الحقل لأن أي اختيار تلقائي قد يفاجئ شخصًا ما. اجعل نطاق القرار صغيرًا عبر عرض الحقول المتعارضة فقط، لا النموذج بأكمله.

كيف تمنع إصدارات السجلات الاستبدالات الصامتة؟

عامل version كعداد متزايد على الخادم للسجل، وطوّب العميل أن يرسل expected_version مع كل تحديث. إذا لم يتطابق إصدار الخادم، ارفض وارجع استجابة تعارض بدلاً من الاستبدال. هذه القاعدة الوحيدة تمنع فقدان البيانات الصامت حتى لو تم تزامن الجهازين بترتيب مختلف.

ما البيانات الوصفية التي يجب أن تتضمنها كل جدول SQLite المتزامن؟

الحد الأدنى العملي هو id ثابت، وversion يتحكم فيه الخادم، وupdated_at/updated_by من جهة الخادم حتى تشرح ما تغيّر. على الجهاز، تتبع ما إذا كانت الصفوف معدلة ومنتظرة للرفع (مثل pending_sync) واحتفظ بآخر إصدار مرسل للخادم. بدون هذه الحقول لا يمكنك اكتشاف التعارضات أو عرض شاشة حل مفيدة.

هل أُزامن السجل كاملًا أم فقط الحقول المتغيرة؟

أرسل فقط الحقول التي تغيّرت (تصحيح) بالإضافة إلى expected_version. تحميل السجل كاملًا يحوّل تعديلات صغيرة غير متداخلة إلى تعارضات غير ضرورية ويزيد احتمال استبدال قيمة أحدث على الخادم ببيانات مخزنة قديمة. التصحيحات توضح أيضًا أي الحقول تحتاج قواعد دمج.

هل من الأفضل تخزين لقطات أم سجل تغييرات لتحريرات بدون اتصال؟

اللقطة أبسط: تخزن الحالة الكاملة الأخيرة وتقارنها بالخادم لاحقًا. سجل العمليات أكثر مرونة: تخزن عمليات مثل "تعيين حقل" أو "إضافة ملاحظة" وتعاد تشغيلها فوق أحدث حالة على الخادم، وهذا يندمج أفضل عادةً للملاحظات والتصنيفات والتحديثات الإضافية. اختر اللقطة للتنفيذ السريع؛ اختر سجل العمليات إذا كانت عمليات الدمج متكررة وتحتاج إلى تاريخ أوضح من الذي غيّر ماذا.

كيف أتعامل مع تعارض الحذف مقابل التعديل؟

قرّر مسبقًا ما إذا كان الحذف أقوى من التعديل، لأن المستخدمين يتوقعون سلوكًا متسقًا. الافتراضي الآمن في كثير من تطبيقات الأعمال هو معاملة الحذف كتومبستون: علم بالحذف عبر deleted_at وإصدار، حتى لا يعيد رفع نسخة قديمة إحياء السجل. إذا أردت قابلية التراجع، استخدم حالة "مؤرشف" بدل الحذف الصلب.

ما أكثر الأخطاء شيوعًا التي تسبب فقدان بيانات التزامن دون اتصال؟

لا ترتب كتابات حساسة بتوقيت الجهاز لأن الساعات تنحرف والمناطق الزمنية تتغير؛ استخدم إصدارات الخادم لترتيب وصلاحية التعارضات. تجنّب LWW في الحقول الحساسة مثل الحالة، المالك، والمجاميع. أيضًا، لا تظهر شاشة مقارنة كاملة عندما يتعارض حقل واحد أو اثنان، لأن هذا يزيد الأخطاء والإحباط.

كيف أنفذ تجربة مستخدم صديقة للتعارضات عند البناء بـ AppMaster؟

احتفظ بالوعد أن Saved يعني محفوظًا محليًا وبيّن حالة منفصلة Synced حتى يفهم المستخدم ما يحدث. إذا بنيت هذا في AppMaster، اتبع نفس البنية: عرّف قواعد دمج لكل حقل، ادمج الحقول الآمنة تلقائيًا، وحوّل فقط الاصطدامات الحقلية الحقيقية إلى خطوة مراجعة صغيرة. اختبر مع جهازين يحرران نفس السجل بدون اتصال وتزامن بترتيب مختلف للتأكد من أن النتائج متوقعة.

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء