20 अग॰ 2025·8 मिनट पढ़ने में

Kotlin + SQLite के लिए ऑफ़लाइन-प्रथम फ़ॉर्म संघर्ष समाधान

ऑफ़लाइन-प्रथम फ़ॉर्म संघर्षों को समझें: स्पष्ट फ़ील्ड-स्तरीय मर्ज नियम, सरल Kotlin + SQLite सिंक फ्लो, और एडिट टकरावों के लिए व्यवहारिक UX पैटर्न।

Kotlin + SQLite के लिए ऑफ़लाइन-प्रथम फ़ॉर्म संघर्ष समाधान

असल में क्या होता है जब दो लोग ऑफ़लाइन संपादन करते हैं

ऑफ़लाइन-प्रथम फ़ॉर्म लोगों को धीमे या अनुपलब्ध नेटवर्क के दौरान भी डेटा देखने और संपादित करने देते हैं। सर्वर का इंतज़ार करने के बजाय ऐप पहले परिवर्तनों को लोकल SQLite डेटाबेस में लिखता है और बाद में सिंक करता है।

यह तात्कालिक महसूस होता है, लेकिन एक सरल वास्तविकता पैदा करता है: दो डिवाइस एक ही रिकॉर्ड को बिना एक दूसरे को जानने के बदल सकते हैं।

एक सामान्य संघर्ष ऐसा दिखता है: एक फील्ड टेक एक टैबलेट पर बेसमेंट में वर्क ऑर्डर खोलता है जहाँ सिग्नल नहीं है। वे status को "Done" करते हैं और एक नोट जोड़ते हैं। उसी समय, एक सुपरवाइज़र दूसरे फोन पर वही वर्क ऑर्डर अपडेट कर रहा है, उसे रीअसाइन कर रहा है और ड्यू डेट एडिट कर रहा है। दोनों Save दबाते हैं। दोनों लोकली सफल होते हैं। किसी ने भी कुछ गलत नहीं किया।

जब सिंक अंततः होता है, तो सर्वर को तय करना होता है कि "वास्तविक" रिकॉर्ड क्या है। यदि आप संघर्षों को स्पष्ट रूप से हैंडल नहीं करते, तो आमतौर पर ये नतीजे होते हैं:

  • Last write wins: बाद वाला सिंक पहले वाले बदलावों को ओवरराइट कर देता है और किसी का डेटा खो जाता है।
  • Hard failure: सिंक एक अपडेट को रिजेक्ट कर देता है और ऐप एक बेकार एरर दिखाता है।
  • Duplicate records: ओवरराइट से बचने के लिए सिस्टम दूसरा कॉपी बना देता है और रिपोर्टिंग गड़बड़ हो जाती है।
  • Silent merge: सिस्टम बदलावों को जोड़ देता है, पर फ़ील्ड्स ऐसे मिलते हैं जो उपयोगकर्ता उम्मीद नहीं करते।

संघर्ष कोई बग नहीं हैं। वे ऑफ़लाइन-प्रथम का पूर्वानुमेय परिणाम हैं — यही ऑफ़लाइन-प्रथम का पूरा उद्देश्य है।

लक्ष्य दोहरी है: डेटा की रक्षा करना और ऐप को उपयोग में आसान रखना। इसका मतलब अक्सर स्पष्ट मर्ज नियम होते हैं (अक्सर फ़ील्ड-स्तर पर) और एक यूएक्स जो तभी लोगों को रोकता है जब वास्तव में ज़रूरी हो। यदि दो एडिट्स अलग फ़ील्ड्स को छूते हैं, तो आप अक्सर शांतिपूर्वक मर्ज कर सकते हैं। यदि दो लोग एक ही फ़ील्ड को अलग तरीके से बदलते हैं, तो ऐप को यह दिखाना चाहिए और किसी को सही परिणाम चुनने में मदद करनी चाहिए।

अपने डेटा के अनुरूप एक संघर्ष रणनीति चुनें

संघर्ष तकनीकी समस्या पहले नहीं होते — ये उत्पाद निर्णय होते हैं कि जब दो लोगों ने एक ही रिकॉर्ड को सिंक से पहले बदला, तो "सही" क्या माना जाएगा।

तीन रणनीतियाँ अधिकांश ऑफ़लाइन ऐप्स को कवर करती हैं:

  • Last write wins (LWW): नवीनतम एडिट स्वीकार करें और पुराने को ओवरराइट कर दें।
  • Manual review: रुकें और किसी मानव से चुनने के लिए कहें कि क्या रखना है।
  • Field-level merge: फ़ील्ड-दर-फ़ील्ड बदलावों को मिलाएँ और केवल तब पूछें जब दोनों ने एक ही फ़ील्ड छुआ हो।

जब गति परफेक्ट सटीकता से ज़्यादा मायने रखती है और गलत होने की लागत कम होती है तो LWW ठीक हो सकता है। सोचें आंतरिक नोट्स, गैर-नाज़ुक टैग्स, या ड्राफ्ट स्टेटस जिन्हें बाद में फिर एडिट किया जा सकता है।

मैनुअल रिव्यू उच्च-प्रभाव वाले फ़ील्ड्स के लिए सुरक्षित विकल्प है जहाँ ऐप अनुमान नहीं लगाना चाहिए: कानूनी टेक्स्ट, अनुपालन पुष्टियाँ, पे-रोल और इनवॉइसिंग राशियाँ, बैंकिंग विवरण, दवा निर्देश और कोई भी चीज़ जो दायित्व पैदा कर सकती है।

फ़ील्ड-स्तरीय मर्ज आम तौर पर उन फ़ॉर्म्स के लिए सबसे अच्छा डिफ़ॉल्ट है जहाँ अलग-अलग रोल अलग हिस्सों को अपडेट करते हैं। एक सपोर्ट एजेंट पता एडिट करता है जबकि सेल्स रिन्यूअल डेट अपडेट करते हैं। प्रति-फ़ील्ड मर्ज दोनों बदलावों को रखता है बिना किसीको परेशान किए। पर यदि दोनों ने रिन्यूअल डेट बदली है, तो उस फ़ील्ड को निर्णय के लिए ट्रिगर करना चाहिए।

कुछ भी लागू करने से पहले, लिख दें कि आपके व्यवसाय के लिए "सही" का क्या अर्थ है। एक तेज़ चेकलिस्ट मदद करती है:

  • किन फ़ील्ड्स को हमेशा नवीनतम वास्तविक-विश्व मान दर्शाना चाहिए (जैसे वर्तमान status)?
  • कौन से फ़ील्ड ऐतिहासिक हैं और कभी ओवरराइट नहीं होने चाहिए (जैसे submitted_at समय)?
  • किसे हर फ़ील्ड बदलने की अनुमति है (भूमिका, मालिकाना, अनुमोदन)?
  • जब मानों में मतभेद हों तो सत्य स्रोत क्या है (डिवाइस, सर्वर, मैनेजर अनुमोदन)?
  • अगर आप गलत चुनते हैं तो क्या होगा (छोटी असुविधा बनाम वित्तीय या कानूनी प्रभाव)?

जब ये नियम स्पष्ट हों, तो सिंक कोड का एक काम होता है: इन्हें लागू करना।

स्क्रीन के बजाय फ़ील्ड-स्तर पर मर्ज नियम परिभाषित करें

जब संघर्ष होता है, तो वह शायद पूरे फ़ॉर्म को एक समान रूप से प्रभावित नहीं करता। एक यूजर फोन नंबर बदल सकता है जबकि दूसरा नोट जोड़ दे। यदि आप पूरे रिकॉर्ड को ऑल-ऑर-नथिंग मानेंगे तो आप लोगों को अच्छे काम को दोहराने के लिए मजबूर कर देंगे।

फ़ील्ड-स्तरीय मर्ज अधिक अनुमान्य है क्योंकि हर फ़ील्ड का एक ज्ञात व्यवहार होता है। UX शांत और तेज़ रहता है।

शुरू करने का एक सरल तरीका यह है कि फ़ील्ड्स को "आम तौर पर सुरक्षित" और "आम तौर पर असुरक्षित" श्रेणियों में बाँट दें।

आम तौर पर स्वतः मर्ज के लिए सुरक्षित: नोट्स और आंतरिक टिप्पणियाँ, टैग्स, संलग्नक (अक्सर यूनियन), और टाइमस्टैम्प जैसे last contacted (अक्सर नवीनतम रखें)।

आम तौर पर स्वतः मर्ज के लिए असुरक्षित: status/state, assignee/owner, totals/prices, approval flags, और inventory counts।

फिर हर फ़ील्ड के लिए एक प्राथमिकता नियम चुनें। सामान्य विकल्प हैं: server wins, client wins, role wins (उदाहरण: manager agent से override कर सकता है), या एक निर्णायक tie-breaker जैसे newest server version।

मुख्य प्रश्न यह है कि जब दोनों पक्षों ने एक ही फ़ील्ड बदला तो क्या होगा। हर फ़ील्ड के लिए एक व्यवहार चुनें:

  • एक स्पष्ट नियम के साथ ऑटो-मार्ज (उदा. टैग्स को यूनियन करें)
  • दोनों मान रखें (उदा. नोट्स को लेखक और समय के साथ जोड़ दें)
  • समीक्षा के लिए फ्लैग करें (उदा. status और assignee पर निर्णय चाहिये)

उदाहरण: दो सपोर्ट रेप्स ऑफ़लाइन एक ही टिकट एडिट करते हैं। रिप A ने status को "Open" से "Pending" किया। रिप B ने notes बदले और tag "refund" जोड़ा। सिंक पर आप notes और tags को सुरक्षित रूप से मर्ज कर सकते हैं, पर status को चुपचाप मर्ज नहीं करना चाहिए। सिर्फ़ status के लिए प्रॉम्प्ट दिखाएँ, बाकी सब पहले से मर्ज हो।

बाद में बहस रोकने के लिए, हर फ़ील्ड के लिए एक वाक्य में नियम दस्तावेज़ करें:

  • "notes: दोनों रखें, नवीनतम को अंत में जोड़ें, लेखक और समय शामिल करें."
  • "tags: यूनियन करें, तभी हटाएँ जब दोनों पक्षों ने स्पष्ट रूप से हटाया हो."
  • "status: यदि दोनों ने बदला तो उपयोगकर्ता से चुनवाएँ."
  • "assignee: manager जीतता है, अन्यथा server जीतता है."

वह एक-वाक्य नियम Kotlin कोड, SQLite क्वेरीज़ और संघर्ष UI के लिए स्रोत सच्चाई बन जाता है।

डेटा मॉडल के मूल तत्व: SQLite में संस्करण और ऑडिट फ़ील्ड्स

यदि आप चाहते हैं कि संघर्ष अनुमान्य महसूस हों, तो हर सिंक किए गए टेबल में कुछ छोटी मेटाडेटा कॉलम जोड़ें। इनके बिना, आप यह नहीं बता पाएँगे कि आप एक नया एडिट देख रहे हैं, एक पुरानी कॉपी, या दोनों एडिट्स जिनको मर्ज की ज़रूरत है।

प्रैक्टिकल न्यूनतम प्रत्येक सर्वर-सिंक रिकॉर्ड के लिए:

  • id (स्थिर प्राथमिक कुंजी): इसे कभी पुन: उपयोग न करें
  • version (integer): सर्वर पर हर सफल लेखन के साथ बढ़ता है
  • updated_at (timestamp): जब रिकॉर्ड आख़िरकार बदला गया
  • updated_by (text या user id): किसने आख़िरी बदलाव किया

डिवाइस पर, लोकल-ओनली फ़ील्ड जोड़ें जो उन बदलावों को ट्रैक करें जिन्हें सर्वर ने पुष्टि नहीं की:

  • dirty (0/1): लोकल बदलाव मौजूद हैं
  • pending_sync (0/1): अपलोड के लिए कतारबद्ध, पर पुष्टि नहीं हुई
  • last_synced_at (timestamp): उस समय का निशान जब यह रो रिकॉर्ड सर्वर से मेल खाता था
  • sync_error (text, वैकल्पिक): UI में दिखाने के लिए आख़िरी विफलता का कारण

Optimistic concurrency सबसे सरल नियम है जो साइलेंट ओवरराइट्स को रोकता है: हर अपडेट के साथ वह संस्करण शामिल करें जिसे आप एडिट कर रहे हैं (expected_version)। यदि सर्वर रिकॉर्ड अभी भी उस संस्करण पर है तो अपडेट स्वीकार कर लिया जाता है और सर्वर नया संस्करण लौटाता है। यदि नहीं, तो यह एक संघर्ष है।

उदाहरण: यूजर A और यूजर B दोनों ने version = 7 डाउनलोड किया। A पहले सिंक करता है; सर्वर 8 कर देता है। जब B expected_version = 7 के साथ सिंक करता है, तो सर्वर इसे संघर्ष के साथ रिजेक्ट कर देता है ताकि B की ऐप ओवरराइट करने के बजाय मर्ज करे।

एक अच्छी संघर्ष स्क्रीन के लिए, साझा प्रारंभिक बिंदु रखें: उपयोगकर्ता किस रिकॉर्ड से शुरू करके एडिट कर रहा था। दो सामान्य दृष्टियाँ:

  • आख़िरी सिंक किए गए रिकॉर्ड का स्नैपशॉट रखें (एक JSON कॉलम या एक समान तालिका)।
  • चेंज लॉग रखें (प्रत्येक एडिट के लिए एक रो या फ़ील्ड-दर-एडिट)।

स्नैपशॉट सरल हैं और अक्सर फ़ॉर्म्स के लिए पर्याप्त होते हैं। चेंज लॉग भारी होते हैं, पर वे ठीक-ठीक बता सकते हैं कि क्या बदला, फ़ील्ड-दर-फ़ील्ड।

किसी भी तरह, UI को हर फ़ील्ड के लिए तीन मान दिखाने में सक्षम होना चाहिए: उपयोगकर्ता का एडिट, सर्वर का वर्तमान मान, और साझा प्रारंभिक बिंदु।

रिकॉर्ड स्नैपशॉट्स बनाम चेंज लॉग: एक तरीका चुनें

Choose your deployment option
AppMaster Cloud, AWS, Azure, Google Cloud पर तैनात करें, या एक्सपोर्ट किए गए कोड से self-host करें।
Deploy app

ऑफ़लाइन-प्रथम फ़ॉर्म्स सिंक करते समय आप पूरा रिकॉर्ड (स्नैपशॉट) अपलोड कर सकते हैं या ऑपरेशन्स की सूची (चेंज लॉग) अपलोड कर सकते हैं। दोनों Kotlin और SQLite के साथ काम करते हैं, पर वे अलग तरह से व्यवहार करते हैं जब दो लोग एक ही रिकॉर्ड एडिट करते हैं।

विकल्प A: पूरा-रिकॉर्ड स्नैपशॉट्स

स्नैपशॉट्स में हर सेव पर नवीनतम पूरा स्टेट (सभी फ़ील्ड) लिखा जाता है। सिंक पर आप रिकॉर्ड और संस्करण नंबर भेजते हैं। अगर सर्वर देखे कि संस्करण पुराना है तो संघर्ष होता है।

यह बनाना सरल है और पढ़ने में तेज़ है, पर अक्सर गैर-आवश्यक संघर्ष बनाता है। यदि यूजर A फोन नंबर बदलता है जबकि यूजर B पता बदलता है, तो स्नैपशॉट अप्रोच इसे एक बड़ा क्लैश मान सकता है हालाँकि एडिट ओवरलैप नहीं करते।

विकल्प B: चेंज लॉग (ऑपरेशन्स)

चेंज लॉग के साथ आप जो बदला उसे स्टोर करते हैं, पूरे रिकॉर्ड को नहीं। हर लोकल एडिट एक ऑपरेशन बन जाता है जिसे आप नवीनतम सर्वर स्टेट के ऊपर रिप्ले कर सकते हैं।

ऐसी ऑपरेशन्स जो अक्सर मर्ज में आसान होती हैं:

  • एक फ़ील्ड वैल्यू सेट करना (उदा. email को नया मान सेट करना)
  • नोट जोड़ना (एक नया नोट आइटम जोड़ना)
  • टैग जोड़ना (सेट में एक टैग जोड़ना)
  • टैग हटाना (सेट में से एक टैग हटाना)
  • चेकबॉक्स पूरा करना (उदा. isDone true सेट करना और समय स्टैम्प जोड़ना)

ऑपरेशन लॉग्स संघर्ष कम कर सकते हैं क्योंकि कई एक्शन ओवरलैप नहीं करते। नोट्स जोड़ना शायद ही किसी और के नोट जोड़ने से टकराता है। टैग जोड़ना और हटाना सेट गणित की तरह मर्ज हो सकता है। सिंगल-वैल्यू फ़ील्ड्स के लिए, फिर भी आपको तब भी पर-फ़ील्ड नियम चाहिए जब दो अलग एडिट टकराएँ।

ट्रेडऑफ़ जटिलता है: स्थिर ऑपरेशन IDs, ऑर्डरिंग (लोकल सीक्वेंस और सर्वर समय), और उन ऑपरेशन्स के लिए नियम जो कम्यूट नहीं करते।

सफाई: सफल सिंक के बाद संकुचन

ऑपरेशन लॉग बढ़ते हैं, इसलिए उन्हें छोटा करने की योजना बनाएं।

एक सामान्य तरीका है प्रति-रिकॉर्ड कम्पैक्शन: एक बार जब सभी ऑपरेशन्स किसी ज्ञात सर्वर संस्करण तक मान्य कर दिए गए हों, उन्हें एक नए स्नैपशॉट में फ़ोल्ड करें और उन पुराने ऑपरेशन्स को डिलीट कर दें। यदि आपको undo, ऑडिटिंग या डिबगिंग के लिए ज़रूरत हो तो केवल एक छोटा हिस्सा रखें।

Kotlin + SQLite के लिए चरण-दर-चरण सिंक फ्लो

Design sync-ready data models
AppMaster के विज़ुअल Data Designer का उपयोग करके संस्करण और ऑडिट फ़ील्ड के साथ PostgreSQL डेटा मॉडल करें।
बनाना शुरू करें

एक अच्छा सिंक रणनीति ज़्यादातर उस पर सख्ती से लागू होने के बारे में है कि आप क्या भेजते हैं और क्या स्वीकार करते हैं। लक्ष्य सरल है: कभी भी गलती से नए डेटा को ओवरराइट न करें, और जब आप सुरक्षित रूप से मर्ज न कर सकें तो संघर्ष स्पष्ट करें।

एक प्रैक्टिकल फ्लो:

  1. हर एडिट को पहले SQLite में लिखें। लोकल ट्रांज़ैक्शन में बदलाव सेव करें और रिकॉर्ड को pending_sync = 1 के रूप में चिह्नित करें। local_updated_at और आख़िरी ज्ञात server_version स्टोर करें।

  2. पूरा रिकॉर्ड भेजने के बजाय पैच भेजें। जब कनेक्टिविटी लौटे, तो रिकॉर्ड id के साथ केवल बदले हुए फ़ील्ड्स और expected_version भेजें।

  3. सर्वर को मिसमैच्ड वर्शन रिजेक्ट करने दें। यदि सर्वर का वर्तमान वर्शन expected_version से मेल नहीं खाता, तो यह एक संघर्ष पेलोड लौटाता है (सर्वर रिकॉर्ड, प्रस्तावित बदलाव, और कौन से फ़ील्ड अलग हैं)। यदि वर्शन मेल खाते हैं, तो यह पैच लागू करता है, संस्करण बढ़ाता है और अपडेटेड रिकॉर्ड लौटाता है।

  4. पहले ऑटो-मार्ज लागू करें, फिर यूज़र से पूछें। फ़ील्ड-स्तरीय मर्ज नियम चलाएँ। सुरक्षित फ़ील्ड्स को नोट्स की तरह अलग तरह से व्यवहार करें बनाम संवेदनशील फ़ील्ड्स जैसे status, कीमतें, या assignee

  5. अंतिम परिणाम कमिट करें और pending फ्लैग साफ़ करें। चाहे ऑटो-मार्ज हुआ हो या मैन्युअल रिज़ॉल्व, अंतिम रिकॉर्ड SQLite में लिखें, server_version अपडेट करें, pending_sync = 0 सेट करें, और इतना ऑडिट डेटा रखें कि बाद में क्या हुआ समझाया जा सके।

उदाहरण: दो सेल्स रेप्स ऑफ़लाइन एक ही ऑर्डर एडिट करते हैं। रेप A delivery date बदलता है। रेप B ग्राहक फोन नंबर बदलता है। पैचेस के साथ सर्वर दोनों बदलावों को साफ़ स्वीकार कर सकता है। यदि दोनों ने delivery date बदली है, तो आप एक स्पष्ट निर्णय दिखाएँगे बजाय इसके कि पूरा रिपोइन्ट फिर से भरा जाए।

UI वादा कंसिस्टेंट रखें: "Saved" का मतलब लोकली सेव हुआ होना चाहिए। "Synced" एक अलग, स्पष्ट स्थिति होनी चाहिए।

फ़ॉर्म संघर्ष हल करने के लिए UX पैटर्न

संघर्ष अपवाद होना चाहिए, सामान्य प्रवाह नहीं। पहले जो सुरक्षित है उसे ऑटो-मर्ज करें, फिर केवल तभी यूज़र से पूछें जब फैसला वास्तव में ज़रूरी हो।

सुरक्षित डिफॉल्ट्स से संघर्ष दुर्लभ बनाएं

यदि दो लोग अलग फ़ील्ड्स एडिट करते हैं, तो बिना मॉडल दिखाए मर्ज कर दें। दोनों बदलाव रखें और एक छोटा "Updated after sync" संदेश दिखाएँ।

प्रॉम्प्ट केवल सच्चे टकरावों के लिए रखें: वही फ़ील्ड दोनों द्वारा बदली गई हो, या एक बदलाव किसी अन्य फ़ील्ड पर निर्भर करता हो (जैसे status + status reason)।

जब पूछना ज़रूरी हो, तो पूरा करना तेज़ रखें

एक संघर्ष स्क्रीन को दो चीज़ों का उत्तर देना चाहिए: क्या बदला और क्या सेव होगा। मानों की साइड-बाय-साइड तुलना करें: "Your edit", "Their edit", और "Saved result"। यदि केवल दो फ़ील्ड टकरा रहे हैं, तो पूरा फ़ॉर्म न दिखाएँ। सीधे उन फ़ील्ड्स पर जाएँ और बाकी को read-only रखें।

क्रियाओं को सीमित रखें:

  • Keep mine
  • Keep theirs
  • Edit final
  • Review field-by-field (जब ज़रूरी हो)

आंशिक मर्ज जहां UX उलझन पैदा करता है। केवल टकराने वाले फ़ील्ड्स हाइलाइट करें और स्रोत स्पष्ट रूप से लेबल करें ("Yours" और "Theirs")। सबसे सुरक्षित विकल्प प्रीसिलेक्ट करें ताकि उपयोगकर्ता पुष्टि करके आगे बढ़ सके।

उम्मीदें सेट करें ताकि उपयोगकर्ता फँसे हुए न महसूस करें। बताएं कि अगर वे छोड़ते हैं तो क्या होगा: उदाहरण के लिए, "हम आपका संस्करण लोकली रखेंगे और बाद में फिर से सिंक कोशिश करेंगे" या "यह रिकॉर्ड Needs review में रहेगा जब तक आप चुनते नहीं"। उस स्थिति को सूची में दिखाईए ताकि संघर्ष गायब न हो जाएँ।

यदि आप यह फ्लो AppMaster में बना रहे हैं, तो वही UX दृष्टिकोण लागू होता है: पहले सुरक्षित फ़ील्ड्स ऑटो-मार्ज करें, फिर केवल विशिष्ट फ़ील्ड्स के टकराव होने पर केंद्रित समीक्षा चरण दिखाएँ।

जटिल मामले: डिलीट, डुप्लिकेट और "मिसिंग" रिकॉर्ड्स

Prototype a field service workflow
एक वर्क-ऑर्डर ऐप प्रोटोटाइप करें जहां टेक्स ऑफ़लाइन संपादित कर सकें और बाद में सुरक्षित रूप से सिंक करें।
शुरू करें

ज्यादातर सिंक इश्यू जो रैंडम लगते हैं वे तीन स्थितियों से आते हैं: किसी ने डिलीट किया जबकि किसी और ने एडिट किया, दो डिवाइस ने ऑफ़लाइन उसी चीज़ की "एक जैसी" प्रविष्टि बना दी, या एक रिकॉर्ड गायब हो जाता है और फिर फिर से प्रकट होता है। इनके लिए स्पष्ट नियम चाहिए क्योंकि LWW अक्सर लोगों को चौंका देता है।

Delete बनाम edit: कौन जीतता है?

निर्र्चय करें कि क्या delete एक एडिट से मजबूत है। कई बिजनेस ऐप्स में delete जीतता है क्योंकि उपयोगकर्ता अपेक्षा करते हैं कि हटाया गया रिकॉर्ड हटाया ही रहे।

एक प्रैक्टिकल नियम सेट:

  • यदि किसी डिवाइस पर रिकॉर्ड डिलीट किया गया है, तो उसे हर जगह डिलीट माना जाए, भले ही बाद के एडिट हों।
  • यदि deletes reversible होने चाहिए, तो हार्ड डिलीट की बजाय deleted_at के साथ एक archived state उपयोग करें।
  • यदि किसी डिलीट के बाद किसी एडिट आता है, तो ऑडिट के लिए एडिट इतिहास रखें, पर रिकॉर्ड को पुनर्स्थापित न करें।

ऑफ़लाइन क्रिएशन टकराव और डुप्लिकेट ड्राफ्ट

ऑफ़लाइन-प्रथम फ़ॉर्म अक्सर सर्वर फ़ाइनल ID देने से पहले अस्थायी IDs (जैसे UUID) बनाते हैं। जब उपयोगकर्ता एक ही वास्तविक-विश्व चीज़ के लिए दो ड्राफ्ट बनाते हैं तो डुप्लिकेट होते हैं।

यदि आपके पास कोई स्थिर नेचुरल की (receipt number, barcode, email+date) है तो उसे टकराव पता करने के लिए उपयोग करें। यदि नहीं, तो स्वीकार करें कि डुप्लिकेट होंगे और बाद में सरल मर्ज विकल्प दें।

इम्प्लीमेंटेशन टिप: SQLite में local_id और server_id दोनों स्टोर करें। जब सर्वर उत्तर दे तो मैपिंग लिखें और तब तक रखें जब तक आप सुनिश्चित न हों कि कोई कतारबद्ध बदलाव लोकल ID को संदर्भित नहीं कर रहा।

सिंक के बाद "resurrection" रोकना

Resurrection तब होता है जब डिवाइस A ने रिकॉर्ड डिलीट किया, पर डिवाइस B ऑफ़लाइन था और बाद में एक पुरानी कॉपी अपलोड कर देता है जिससे रिकॉर्ड फिर से बन जाता है।

फिक्स है एक tombstone। पंक्ति तुरंत हटाने की बजाय उसे deleted_at (अक्सर deleted_by और delete_version भी) से चिह्नित करें। सिंक के दौरान, tombstones को वास्तविक बदलाव के रूप में ट्रीट करें जो पुराने नॉन-डिलीट स्टेट्स को ओवरराइड कर सकें।

निर्णय लें कि tombstones कब तक रखें। यदि उपयोगकर्ता हफ्तों तक ऑफ़लाइन रह सकते हैं, तो उन्हें उससे भी अधिक रखें। केवल तभी purge करें जब आप आश्वस्त हों कि सक्रिय डिवाइस उस डिलीट से आगे सिंक कर चुके हैं।

यदि आप undo सपोर्ट करते हैं, तो undo को एक और बदलाव की तरह ट्रीट करें: deleted_at क्लियर करें और संस्करण बढ़ाएँ।

डेटा लॉस या उपयोगकर्ता निराशा पैदा करने वाली सामान्य गलतियाँ

Prevent silent overwrites
पैच-स्टाइल अपडेट और संस्करण जांच सेट अप करें ताकि नया डेटा मौन रूप से ओवरराइट न हो सके।
बनाना शुरू करें

कई सिंक फेलियर्स छोटे अनुमानों से आते हैं जो चुपचाप अच्छे डेटा को ओवरराइट कर देते हैं।

गलती 1: एडिट्स की क्रमबद्धता के लिए डिवाइस समय पर भरोसा करना

फोन के क्लॉक्स गलत हो सकते हैं, टाइमज़ोन बदलते हैं, और उपयोगकर्ता मैन्युअली समय सेट कर सकते हैं। यदि आप डिवाइस टाइमस्टैम्प्स के आधार पर बदलावों को ऑर्डर करते हैं, तो आप अंततः गलत क्रम में एडिट्स लागू करेंगे।

सर्वर-इश्यूड वर्शन (serverVersion) को प्राथमिकता दें और क्लाइंट टाइमस्टैम्प्स को केवल डिस्प्ले-ओनली रखें। यदि आपको समय का उपयोग करना ही है तो सर्वर पर safeguards और reconcile जोड़ें।

गलती 2: संवेदनशील फ़ील्ड्स पर आकस्मिक LWW

LWW साधा लगता है जब तक कि वह उन फ़ील्ड्स से न टकरा जाए जिनका "जीतना" किसी भी व्यक्ति के बाद सिंक होने पर नुकसान पहुँचा सकता है। status, totals, approvals और assignments आमतौर पर स्पष्ट नियम या मैन्युअल रिव्यू मांगते हैं।

उच्च-जोखिम फ़ील्ड्स के लिए एक सुरक्षा चेकलिस्ट:

  • status ट्रांज़िशन्स को स्टेट मशीन की तरह ट्रीट करें, फ्री-टेक्स्ट एडिट की तरह नहीं।
  • totals को line items से पुनः गणना करें। totals को कच्चे नंबर की तरह मर्ज न करें।
  • काउंटर के लिए, डेल्टा लागू करके मर्ज करें, विजेता चुनकर नहीं।
  • ownership या assignee के लिए, संघर्ष पर स्पष्ट पुष्टि माँगें।

गलती 3: स्टेल कैश्ड डेटा से नए सर्वर मानों को ओवरराइट करना

यह तब होता है जब क्लाइंट एक पुरानी स्नैपशॉट एडिट करता है, फिर पूरा रिकॉर्ड अपलोड कर देता है। सर्वर इसे स्वीकार कर लेता है और नए सर्वर-साइड बदलाव गायब हो जाते हैं।

जो आप भेजते हैं उसकी शक्ल ठीक करें: केवल बदले हुए फ़ील्ड्स (या चेंज लॉग) भेजें, साथ में बेस संस्करण जो आपने एडिट किया था। यदि बेस वर्शन पीछे है, तो सर्वर रिजेक्ट या मर्ज करने के लिए कहे।

गलती 4: "किसने क्या बदला" इतिहास न रखना

जब संघर्ष होते हैं, उपयोगकर्ता एक जवाब चाहते हैं: मैंने क्या बदला, और दूसरे ने क्या बदला? बिना editor identity और प्रति-फ़ील्ड परिवर्तन के, आपकी संघर्ष स्क्रीन अटकलों पर निर्भर होती है।

updatedBy, सर्वर-साइड अपडेट समय (यदि है), और कम से कम हल्का प्रति-फ़ील्ड ऑडिट ट्रेल स्टोर करें।

गलती 5: पूरा रिकॉर्ड तुलना करने वाली संघर्ष UI

लोगों को पूरे रिकॉर्ड की तुलना करने पर थकान हो जाती है। अधिकांश संघर्ष केवल एक से तीन फ़ील्ड के होते हैं। केवल टकराने वाले फ़ील्ड्स दिखाएँ, सबसे सुरक्षित विकल्प प्रीसिलेक्ट करें, और बाकी को ऑटो-एक्सेप्ट होने दें।

यदि आप नो-कोड टूल जैसे AppMaster में फ़ॉर्म्स बना रहे हैं, तो उद्देश्य वही रखें: उपयोगकर्ताओं को पूरा फ़ॉर्म देखने के बजाय फ़ील्ड-स्तर पर स्पष्ट विकल्प दें ताकि वे एक स्पष्ट निर्णय लें।

त्वरित चेकलिस्ट और अगले कदम

यदि आप चाहते हैं कि ऑफ़लाइन एडिट सुरक्षित महसूस हों, तो संघर्षों को एक सामान्य स्थिति मानें, त्रुटि नहीं। सबसे अच्छे नतीजे स्पष्ट नियमों, दोहराए जाने योग्य परीक्षणों और उस UX से आते हैं जो सामान्य भाषा में बताता है कि क्या हुआ।

फ़ीचर जोड़ने से पहले, सुनिश्चित करें कि ये बुनियादी बातें लॉक हैं:

  • हर रिकॉर्ड प्रकार के लिए, फ़ील्ड-दर-फ़ील्ड मर्ज नियम असाइन करें (LWW, keep max/min, append, union, या हमेशा पूछें)।
  • एक सर्वर-नियंत्रित version और एक updated_at रखें जिसे आप नियंत्रित करें, और सिंक के दौरान उनकी वैधता जांचें।
  • एक दो-डिवाइस टेस्ट चलाएँ जहाँ दोनों ऑफ़लाइन एक ही रिकॉर्ड एडिट करें और फिर दोनों क्रमों में (A फिर B, B फिर A) सिंक करें। परिणाम अनुमान्य होने चाहिए।
  • हार्ड संघर्षों का परीक्षण करें: delete बनाम edit, और अलग फ़ील्ड्स पर edit बनाम edit।
  • स्थिति स्पष्ट रखें: Synced, Pending upload, और Needs review दिखाएँ।

एक वास्तविक फ़ॉर्म के साथ पूरा फ्लो एंड-टू-एंड प्रोटोटाइप करें, डेमो स्क्रीन नहीं। एक यथार्थवादी परिदृश्य लें: एक फील्ड टेक फोन पर जॉब नोट अपडेट करता है जबकि एक डिस्पैचर टैबलेट पर उसी जॉब का शीर्षक एडिट करता है। यदि वे अलग फ़ील्ड्स छूते हैं तो ऑटो-मार्ज करें और एक छोटा "Updated from another device" संकेत दिखाएँ। यदि उन्होंने एक ही फ़ील्ड छुआ है तो एक सरल समीक्षा स्क्रीन पर रूट करें जिसमें दो विकल्प और स्पष्ट प्रीव्यू हो।

जब आप पूरी मोबाइल ऐप और बैकएंड APIs एक साथ बनाना चाहें, AppMaster (appmaster.io) मदद कर सकता है। आप डेटा मॉडल कर सकते हैं, बिजनेस लॉजिक परिभाषित कर सकते हैं, और वेब व नेटिव मोबाइल UI एक ही जगह बना कर डिप्लॉय या सोर्स कोड एक्सपोर्ट कर सकते हैं जब आपके सिंक नियम पक्के हो जाएँ।

सामान्य प्रश्न

What is an offline sync conflict, in plain terms?

एक संघर्ष तब होता है जब दो डिवाइस वही सर्वर-समर्थित रिकॉर्ड बदलते हैं जबकि वे ऑफ़लाइन होते हैं (या दोनों में से किसी ने अभी तक सिंक नहीं किया होता), और बाद में सर्वर देखता है कि दोनों अपडेट किसी पुराने संस्करण पर आधारित थे। सिस्टम को फिर हर भिन्न फ़ील्ड के लिए अंतिम मान क्या होगा तय करना पड़ता है।

Which conflict strategy should I choose: last write wins, manual review, or field-level merge?

अधिकांश व्यावसायिक फ़ॉर्म के लिए डिफ़ॉल्ट के रूप में field-level merge से शुरू करें, क्योंकि विभिन्न रोल अक्सर अलग-अलग फ़ील्ड अपडेट करते हैं और आप बिना किसीको परेशान किए दोनों परिवर्तन रख सकते हैं। Manual review केवल उन फ़ील्ड्स के लिए उपयोग करें जिनसे वास्तविक नुकसान हो सकता है (पैसे, स्वीकृतियाँ, अनुपालन)। Last write wins केवल उन कम-जोखिम फ़ील्ड्स के लिए ठीक है जहाँ पुराना एडिट खो जाना स्वीकार्य हो।

When should the app ask the user to resolve a conflict?

यदि दो एडिट अलग-अलग फ़ील्ड्स को छूते हैं, तो आप आम तौर पर स्वतः मर्ज कर सकते हैं और UI शांत रख सकते हैं। यदि दो एडिट एक ही फ़ील्ड को अलग-अलग मानों में बदलते हैं, तो उस फ़ील्ड को निर्णय के लिए ट्रिगर करना चाहिए, क्योंकि कोई भी ऑटो-चयन किसी को आश्चर्यचकित कर सकता है। निर्णय का दायरा छोटा रखें — केवल टकरा रहे फ़ील्ड दिखाएँ, पूरा फ़ॉर्म नहीं।

How do record versions prevent silent overwrites?

सर्वर का version रिकॉर्ड के लिए एक monotonically बढ़ने वाला काउंटर माना जाए और क्लाइंट हर अपडेट के साथ expected_version भेजे। यदि सर्वर का वर्तमान संस्करण मेल नहीं खाता, तो ओवरराइट करने के बजाय सर्वर कन्फ़्लिक्ट रेस्पॉन्स भेज कर अपडेट अस्वीकार कर दे। यह नियम “साइलेंट डेटा लॉस” को रोकता है।

What metadata should every synced SQLite table include?

एक व्यावहारिक न्यूनतम सेट में स्थिर id, सर्वर-नियंत्रित version, और सर्वर-नियंत्रित updated_at/updated_by शामिल होने चाहिए ताकि आप बता सकें क्या बदला। डिवाइस पर, यह ट्रैक करें कि पंक्ति बदली है और अपलोड के लिए प्रतीक्षा में है (उदा. pending_sync) और आख़िरी सिंक किए गए सर्वर संस्करण को रखें। इनके बिना आप संघर्षों का पता भरोसेमंद तरीके से नहीं लगा पाएँगे।

Should I sync the whole record or only changed fields?

सिर्फ़ बदले हुए फ़ील्ड्स (एक पैच) और बेस expected_version भेजें। पूरा-रिकॉर्ड अपलोड छोटे, न-अनुपरिवर्ती एडिट्स को बेवजह संघर्ष में बदल देता है और पुराने कैश्ड डेटा से नए सर्वर मानों को ओवरराइट करने का जोखिम बढ़ाता है। पैच भेजने से यह भी साफ़ होता है कि कौन से फ़ील्ड्स के लिए मर्ज नियम लागू होने चाहिए।

Is it better to store snapshots or a change log for offline edits?

स्नैपशॉट सरल है: आप नवीनतम पूरा रिकॉर्ड संग्रहित करते हैं और बाद में सर्वर से तुलना करते हैं। चेंज-लॉग ज़्यादा लचीला है: आप ऑपरेशन्स (जैसे “set field” या “append note”) स्टोर करते हैं और उन्हें नवीनतम सर्वर स्टेट पर रिप्ले करते हैं; यह नोट्स, टैग्स और अन्य एडिट्स के लिए अक्सर बेहतर मर्ज देता है। इम्प्लीमेंटेशन की गति के लिए स्नैपशॉट चुनें; यदि मर्ज अक्सर हों और आपको स्पष्ट “किसने क्या बदला” चाहिए तो चेंज-लॉग चुनें।

How should I handle delete vs edit conflicts?

पहले तय कर लें कि delete किसी edit से मजबूत है या नहीं, क्योंकि उपयोगकर्ता सुसंगत व्यवहार की उम्मीद करते हैं। कई बिजनेस ऐप्स के लिए सुरक्षित डिफ़ॉल्ट यह है कि delete को tombstone के रूप में रखें (deleted_at और एक संस्करण के साथ), ताकि एक पुराना ऑफ़लाइन upsert रिकॉर्ड को गलती से वापस न ला सके। अगर रीवर्सिबिलिटी चाहिए तो hard delete की बजाय “archived” स्थिति उपयोग करें।

What are the most common mistakes that cause offline sync data loss?

डिवाइस समय पर भरोसा न करें ताकि एडिट्स को क्रमबद्ध करें — क्लॉक गलत हो सकते हैं। ऑर्डरिंग और कन्फ्लिक्ट चेक के लिए सर्वर-इश्यूड वर्शन का उपयोग करें और क्लाइंट टाइमस्टैम्प को केवल डिस्प्ले के लिए रखें।

How can I implement a conflict-friendly UX when building with AppMaster?

याद रखें कि “Saved” का मतलब लोकल पर सेव हुआ है और “Synced” एक अलग, स्पष्ट स्थिति होनी चाहिए ताकि उपयोगकर्ता समझ सकें क्या हो रहा है। AppMaster में यह फ्लो बनाते समय भी यही संरचना रखें: फ़ील्ड-स्तरीय मर्ज नियम परिभाषित करें, सुरक्षित फ़ील्ड्स को ऑटो-मर्ज करें, और केवल सच्चे फ़ील्ड टकरावों को छोटे रिव्यू स्टेप पर भेजें। दो डिवाइस से ऑफ़लाइन एडिट कर के और दोनों आदेशों (A फिर B, B फिर A) में सिंक कर के परिक्षण करें ताकि परिणाम अनुमान्य हों।

शुरू करना आसान
कुछ बनाएं अद्भुत

फ्री प्लान के साथ ऐपमास्टर के साथ प्रयोग करें।
जब आप तैयार होंगे तब आप उचित सदस्यता चुन सकते हैं।

शुरू हो जाओ