कभी-कभी अनूठी स्थितियां और हमारे नियंत्रण से बाहर की चीजें बेतहाशा अनैतिक आवश्यकताओं को जन्म देती हैं। हाल ही में, मेरा एक अनुभव था जहां मुझे डेटाबेस आईडी पर भरोसा किए बिना किसी भी रिकॉर्ड के लिए ActiveRecord का उपयोग करने की आवश्यकता थी। अगर कोई ऐसा करने पर विचार कर रहा है, तो मैं अत्यधिक अनुशंसा करता हूं कि दूसरा रास्ता खोजें! लेकिन, आइए कहानी के बाकी हिस्से पर चलते हैं।
निर्णय लिए गए। छोटे डेटाबेस (संरचना में क्लोन लेकिन डेटा में नहीं) को मर्ज करने की आवश्यकता थी। मैं प्रोजेक्ट में ठीक उसी समय शामिल हुआ जब टीम एक स्क्रिप्ट पर फिनिशिंग टच डाल रही थी जो एक डेटाबेस से दूसरे में डेटाबेस रिकॉर्ड्स को कॉपी और पेस्ट करती है। इसने सब कुछ बिल्कुल वैसा ही कॉपी किया, आईडी सहित।
डेटाबेस A
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | orange | 456 |
| ... | ... | ... |
डेटाबेस B
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | banana | 74 |
| ... | ... | ... |
मर्ज के बाद डेटाबेस A
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | orange | 456 |
| 123 | banana | 74 |
| ... | ... | ... |
यह आईडी होने के मूलभूत कारण को तोड़ता है: विशिष्ट पहचान। मुझे विशिष्टताएं नहीं पता थीं, लेकिन मुझे लगा कि एक बार डुप्लीकेट आईडी सिस्टम में पेश होने के बाद सभी प्रकार की समस्याएं दिखाई देंगी। मैंने कुछ कहने की कोशिश की, लेकिन मैं प्रोजेक्ट में नया था, और अन्य लोग निश्चित लग रहे थे कि यह सबसे अच्छा रास्ता था। कुछ दिनों में, हम कोड डेप्लॉय करने जा रहे थे और डुप्लीकेट आईडी के साथ डेटा को संभालना शुरू कर देंगे। सवाल अब नहीं था, "क्या हमें यह करना चाहिए?"; बल्कि, सवाल थे, "हम यह कैसे करते हैं?" और "इसमें कितना अधिक समय लगेगा?"
डुप्लीकेट आईडी के साथ काम करना
तो, आप डुप्लीकेट आईडी वाले डेटा को कैसे संभालते हैं? समाधान कई फ़ील्ड्स का एक कंपोजिट आईडी बनाना था। हमारे अधिकांश DB फेच इस तरह दिखते थे:
# यह काम नहीं करता, user_id: 123 वाले 2 उपयोगकर्ता हो सकते हैं
FavoriteFruit.find(123)
# एकाधिक आईडी क्वेरी को सही रिकॉर्ड तक सीमित करती हैं
FavoriteFruit.find_by(id: 123, user_id: 456)
सभी ActiveRecord कॉल्स को इस तरह अपडेट किया गया था, और जैसा कि मैंने कोड पर नज़र डाली, यह समझ में आया। जब तक हमने इसे डेप्लॉय नहीं किया।
सब नरक टूट पड़ता है
कोड डेप्लॉय करने के तुरंत बाद, फोन बजने लगे। ग्राहक ऐसी संख्याएं देख रहे थे जो जुड़ नहीं रही थीं। वे अपने रिकॉर्ड्स अपडेट नहीं कर सके। सभी प्रकार की सुविधाएं टूट रही थीं।
हमें क्या करना चाहिए? हमने सिर्फ कोड डेप्लॉय नहीं किया; हमने एक डेटाबेस से दूसरे में डेटा भी स्थानांतरित किया (और नया डेटा डेप्लॉय के बाद बनाया/अपडेट किया गया)। यह एक साधारण रोलबैक स्थिति नहीं थी। हमें तेजी से चीजों को ठीक करने की आवश्यकता थी।
रेल्स क्या कर रहा है?
डिबगिंग में पहला कदम वर्तमान व्यवहार को देखना था और त्रुटि को कैसे पुन: पेश किया जाए। मैंने प्रोडक्शन डेटा का एक क्लोन लिया और एक रेल्स कंसोल शुरू किया। आपके सेटअप के आधार पर, जब आप एक ActiveRecord क्वेरी निष्पादित करते हैं तो आप स्वचालित रूप से रेल्स द्वारा चलाए गए SQL क्वेरीज़ नहीं देख सकते हैं। यहां बताया गया है कि अपने कंसोल पर SQL स्टेटमेंट्स को कैसे दृश्यमान बनाया जाए:
ActiveRecord::Base.logger = Logger.new(STDOUT)
उसके बाद, मैंने कुछ सामान्य रेल्स क्वेरीज़ आज़माईं:
$ FavoriteFruit.find_by(id: 123, user_id: 456)
FavoriteFruit Load (0.6ms) SELECT "favorite_fruits".* FROM "favorite_fruits" WHERE "favorite_fruits"."id" = $1 AND "favorite_fruits"."user_id" = $2 [["id", "123"], ["user_id", "456"]]
find_by ठीक काम करता दिख रहा था, लेकिन फिर मैंने ऐसा कोड देखा:
fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
...
...
fruit.reload
उस reload ने मुझे उत्सुक बना दिया, इसलिए मैंने उसका भी परीक्षण किया:
$ fruit.reload
FavoriteFruit Load (0.3ms) SELECT "favorite_fruits".* FROM "favorite_fruits" WHERE "favorite_fruits"."id" = $1 LIMIT $2 [["id", 123], ["LIMIT", 1]]
उह ओह। तो, भले ही हमने find_by के साथ सही रिकॉर्ड को शुरू में फेच किया, जब भी हमने reload को कॉल किया, यह रिकॉर्ड की आईडी लेगा और एक साधारण find-by-id क्वेरी करेगा, जो निश्चित रूप से हमारे डुप्लीकेट आईडी के कारण अक्सर गलत डेटा देगा।
इसने ऐसा क्यों किया? मैंने सुराग के लिए रेल्स सोर्स कोड की जांच की। रूबी ऑन रेल्स के साथ कोडिंग का यह एक बड़ा पहलू है, सोर्स कोड प्लेन रूबी है और एक्सेस करने के लिए स्वतंत्र रूप से उपलब्ध है। मैंने बस "ActiveRecord reload" गूगल किया और जल्दी से यह मिला:
# File activerecord/lib/active_record/persistence.rb, line 602
def reload(options = nil)
self.class.connection.clear_query_cache
fresh_object = if options && options[:lock]
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
@attributes = fresh_object.instance_variable_get("@attributes")
@new_record = false
self
end
यह दर्शाता है कि reload कमोबेश self.class.find(id) के लिए एक वॉपर है। केवल आईडी द्वारा क्वेरी करना इस मेथड में हार्डवायर्ड था। हमारे लिए डुप्लीकेट आईडी के साथ काम करने के लिए, हमें या तो कोर रेल्स मेथड्स को ओवरराइड करना होगा (कभी अनुशंसित नहीं) या reload का उपयोग पूरी तरह से बंद करना होगा।
हमारा समाधान
तो, हमने कोड में हर reload को खोजने और उसे find_by में बदलने का फैसला किया ताकि डेटाबेस फेचिंग कई कुंजियों के माध्यम से हो।
हालांकि, यह केवल कुछ बग्स हल हुए थे। अधिक खुदाई के बाद, मैंने अपने update कॉल्स का परीक्षण करने का फैसला किया:
$ fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
$ fruit.update(last_eaten: Time.now)
FavoriteFruit Update (43.3ms) UPDATE "favorite_fruits" SET "last_eaten" = $1 WHERE "favorite_fruits"."id" = $2 [["updated_at", "2020-04-16 06:24:57.989195"], ["id", 123]]
उह ओह। आप देख सकते हैं कि भले ही find_by ने विशिष्ट फ़ील्ड्स द्वारा रिकॉर्ड को सीमित किया, जब हम रेल्स रिकॉर्ड पर update को कॉल करते हैं, तो इसने एक साधारण WHERE id = x क्वेरी बनाई, जो डुप्लीकेट आईडी के साथ भी टूट जाती है। हम इससे कैसे निपटे?
हमने एक कस्टम अपडेट मेथड बनाया, update_unique, जो ऐसा दिखता है:
class FavoriteFruit
def update_unique(attributes)
run_callbacks :save do
self.class
.where(id: id, user_id: user_id)
.update_all(attributes)
end
self.class.find_by(id: id, user_id: user_id)
end
end
जिसने हमें रिकॉर्ड्स को केवल आईडी से अधिक तक सीमित करके अपडेट करने की अनुमति दी:
$ fruit.update_unique(last_eaten: Time.now)
FavoriteFruit Update All (3.2ms) UPDATE "favorite_fruits" SET "last_eaten" = '2020-04-16 06:24:57.989195' WHERE "favorite_fruits"."id" = $1 AND "favorite_fruits"."user_id" = $2 [["id", "123"], ["user_id", "456"]]
यह कोड रिकॉर्ड्स को अपडेट करने के लिए एक संकीर्ण दायरा सुनिश्चित करता है, लेकिन क्लास के update_all मेथड को कॉल करके, हम उन कॉलबैक्स को खो देते हैं जो आमतौर पर रिकॉर्ड अपडेट करने के साथ आते हैं। इसलिए, हमें मैन्युअली कॉलबैक्स चलाने और अपडेटेड रिकॉर्ड को पुनः प्राप्त करने के लिए एक और डेटाबेस कॉल करना पड़ा क्योंकि update_all अपडेटेड रिकॉर्ड को वापस नहीं करता है। अंतिम उत्पाद बहुत गन्दा नहीं है, लेकिन यह निश्चित रूप से fruit.update की तुलना में पढ़ने में अधिक कठिन है।
वास्तविक समाधान
डूबे हुए लागत, प्रबंधन, और समय की बाधाओं के कारण, हमारा समाधान रेल्स को सभी डेटाबेस कॉल्स के लिए कई कुंजियों का उपयोग करने के लिए मंकी पैच करना था। यह काम करता था, इस अर्थ में कि ग्राहक अभी भी उत्पाद खरीदेंगे और उपयोग करेंगे, लेकिन यह कई कारणों से एक बुरा विचार था:
- कोई भी भविष्य का विकास सामान्य रेल्स मेथड्स का उपयोग करके अनजाने में बग्स को फिर से पेश कर सकता है। नए डेवलपर्स को कोड को
reloadमेथड जैसे छिपे हुए बग्स से मुक्त रखने के लिए सख्त प्रशिक्षण की आवश्यकता होगी। - कोड अधिक जटिल, कम स्पष्ट, और कम रखरखाव योग्य है। यह तकनीकी ऋण है जो प्रोजेक्ट के आगे बढ़ने के साथ विकास गति को धीमा करता जाता है।
- टेस्टिंग बहुत धीमी हो जाती है। आपको न केवल यह टेस्ट करना होगा कि एक फंक्शन काम करता है बल्कि यह भी कि यह तब काम करता है जब विभिन्न ऑब्जेक्ट्स में डुप्लीकेट आईडी हों। टेस्ट लिखने में अधिक समय लगता है, और फिर हर बार टेस्ट सूट चलाने पर, सभी अतिरिक्त टेस्ट्स के माध्यम से चलने में अधिक समय लगता है। टेस्टिंग आसानी से बग्स को मिस कर सकती है यदि प्रोजेक्ट पर प्रत्येक डेवलपर सभी संभावित परिदृश्यों को सावधानीपूर्वक टेस्ट नहीं करता।
इस समस्या का वास्तविक समाधान पहले स्थान पर कभी भी डुप्लीकेट आईडी न रखना है। यदि डेटा को एक डेटाबेस से दूसरे में स्थानांतरित करने की आवश्यकता है, तो ऐसा करने वाली स्क्रिप्ट को आईडी के बिना डेटा एकत्र करना और सम्मिलित करना चाहिए, जिससे प्राप्त करने वाला डेटाबेस अपने मानकीकृत ऑटो-इन्क्रीमेंट काउंटर का उपयोग करके प्रत्येक रिकॉर्ड को अपनी विशिष्ट आईडी दे सके।
एक अन्य समाधान सभी रिकॉर्ड्स के लिए UUIDs का उपयोग करना होगा। इस प्रकार की आईडी यादृच्छिक रूप से बनाया गया वर्णों का एक लंबा स्ट्रिंग है (पूर्णांक आईडी के साथ चरण-दर-चरण गिनती के बजाय)। फिर, डेटा को अन्य डेटाबेस में ले जाने में कोई संघर्ष या समस्या नहीं होगी।
निचला रेखा यह है कि रेल्स को इस समझ के साथ बनाया गया था कि आईडी प्रति रिकॉर्ड विशिष्ट हैं और डेटाबेस में विशिष्ट डेटा में हेरफेर करने का एक त्वरित और आसान तरीका है। रेल्स एक राय वाला फ्रेमवर्क है, और इसकी सुंदरता यह है कि सब कुछ कितनी आसानी से चलता है, जब तक आप रेल्स के तरीके से चीजें करते हैं। यह न केवल रेल्स पर बल्कि प्रोग्रामिंग के कई अन्य पहलुओं पर भी लागू होता है। जब चीजें जटिल हो जाती हैं, तो हमें पता होना चाहिए कि समस्या की पहचान कैसे करें; हालांकि, यदि हम स्पष्ट, रखरखाव योग्य, और पारंपरिक कोड लिखते हैं, तो हम पहली जगह में इनमें से कई जटिलताओं से बच सकते हैं।