क्लाइंट वहां कैसे पहुंचा?
विवरण में गोता लगाने से पहले, आइए यह समझने की कोशिश करें कि एक ऐप इस स्थिति में कैसे पहुंच सकता है। हम एक साधारण users टेबल से शुरू करते हैं। कुछ हफ्तों के बाद, हमें अंतिम साइन-इन समय निर्धारित करने में सक्षम होने की आवश्यकता होती है इसलिए हम users.last_sign_in_at जोड़ते हैं। फिर हमें उपयोगकर्ता का नाम जानने की आवश्यकता होती है। हम first_name और last_name जोड़ते हैं। ट्विटर हैंडल? एक और कॉलम। GitHub प्रोफाइल? फोन नंबर? कुछ महीनों के बाद टेबल दिमाग को चकरा देने वाली बन जाती है।
इसमें क्या गलत है?
इतनी बड़ी टेबल कई समस्याओं का संकेत देती है:
Userकी कई असंबंधित जिम्मेदारियां हैं। इससे इसे समझना, बदलना और परीक्षण करना अधिक कठिन हो जाता है।- ऐप और डेटाबेस के बीच डेटा का आदान-प्रदान अतिरिक्त बैंडविड्थ की आवश्यकता होती है।
- ऐप को भारी मॉडल को स्टोर करने के लिए अधिक मेमोरी की आवश्यकता होती है।
ऐप हर अनुरोध पर प्रमाणीकरण और प्राधिकरण उद्देश्यों के लिए User लाता था लेकिन आमतौर पर केवल कुछ कॉलम का उपयोग करता था। समस्या को ठीक करने से डिज़ाइन और प्रदर्शन दोनों में सुधार होगा।
एक टेबल निकालना
हम इस समस्या को कम उपयोग किए जाने वाले कॉलम को एक नई टेबल (या टेबल्स) में निकालकर हल कर सकते हैं। उदाहरण के लिए, हम प्रोफ़ाइल जानकारी (first_name, आदि) को profiles में निम्न चरणों के साथ निकाल सकते हैं:
- प्रोफ़ाइल-संबंधित कॉलम को
usersमें डुप्लिकेट करने वाले कॉलम के साथprofilesबनाएं। usersमेंprofile_idजोड़ें। इसे अभीNULLपर सेट करें।usersमें प्रत्येक पंक्ति के लिए,profilesमें एक पंक्ति डालें जो प्रोफ़ाइल-संबंधित कॉलम को डुप्लिकेट करती है।usersमें संबंधित पंक्ति केprofile_idको चरण 3 में डाली गई पंक्ति पर इंगित करें।- नहीं
users.profile_idको नॉन-NULLबनाएं। ऐप अभी तक इसके अस्तित्व से अवगत नहीं है इसलिए यह टूट जाएगा।
हमें users.first_name के संदर्भ को profiles.first_name से बदलने की आवश्यकता है और इसी तरह। यदि हम केवल कुछ कॉलम निकाल रहे हैं जिनके कुछ संदर्भ हैं तो मैं मैन्युअल रूप से ऐसा करने की सलाह देता हूं। लेकिन जैसे ही हम खुद को सोचते हुए पकड़ते हैं "ओह, नहीं। यह अब तक का सबसे खराब काम है!" हमें एक विकल्प की तलाश करनी चाहिए।
समस्या की उपेक्षा न करें। कोड का एक हिस्सा जिससे हर कोई बचता है, वह और खराब होगा और और भी अधिक उपेक्षा से ग्रस्त होगा। दुष्चक्र को तोड़ने का सबसे आसान तरीका छोटे से शुरू करना है।
पढ़ें, यदि आप उत्सुक हैं कि मेरे क्लाइंट ने समस्या को कैसे हल किया।
एक बार में एक लाइन कोड ठीक करना
सबसे वृद्धिशील दृष्टिकोण एक समय में पुराने कॉलम के एक संदर्भ को ठीक करना है। आइए first_name को users से profiles में ले जाने पर ध्यान केंद्रित करें।
सबसे पहले, इसके साथ Profile बनाएं:
rails generate model Profile first_name:string
फिर users से profiles में एक संदर्भ जोड़ें और users.first_name को profiles में कॉपी करें:
class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration
# प्रोडक्शन कोड पर निर्भरता तोड़ने के लिए मॉडल को पुनः परिभाषित करें। हमें
# कॉलबैक आदि के बिना वेनिला मॉडल चाहिए। साथ ही, भविष्य में एक मॉडल को हटाने से
# माइग्रेशन टूट सकता है।
class User < ActiveRecord::Base; end
class Profile < ActiveRecord::Base; end
def up
add_reference :users, :profile, index: true, unique: true, foreign_key: true
User.find_each do |user|
profile = Profile.create!(first_name: user.first_name)
user.update!(profile_id: profile.id)
end
change_column_null :users, :profile_id, false
end
def down
remove_reference :users, :profile
end
end
क्योंकि यह प्रत्येक उपयोगकर्ता को ठीक एक प्रोफ़ाइल रखने के लिए मजबूर करता है, users से profiles में एक संदर्भ विपरीत संदर्भ के लिए बेहतर है।
डेटाबेस संरचना के साथ, हम User से Profile को first_name सौंप सकते हैं। मेरे क्लाइंट की कई आवश्यकताएं थीं:
- एक्सेसर्स को संबद्ध
Profileका उपयोग करना चाहिए। उन्हें यह भी लॉग करना चाहिए कि डिप्रिकेटेड एक्सेसर को कहां से कॉल किया गया था। Userको सेव करने से स्वचालित रूप सेProfileसेव होना चाहिए ताकि डिप्रिकेटेड एक्सेसर्स का उपयोग करने वाले कोड को तोड़ने से बचा जा सके।User#first_name_changed?और अन्यActiveModel::Dirtyविधियों को अभी भी काम करना चाहिए।
इसका मतलब है User को इस तरह दिखना चाहिए:
class User < ActiveRecord::Base
# हमें autosave की आवश्यकता है क्योंकि क्लाइंट कोड Profile#first_name से अनजान हो सकता है
# और अभी भी User#first_name का संदर्भ दे सकता है।
belongs_to :profile, autosave: true
def first_name
log_backtrace(:first_name)
profile.first_name
end
def first_name=(new_first_name)
log_backtrace(:first_name)
# super कॉल करें ताकि User#first_name_changed? और इसी तरह अपेक्षा के अनुरूप काम करते रहें।
super
profile.first_name = new_first_name
end
private
def log_backtrace(name)
filtered_backtrace = caller.select do |item|
item.start_with?(Rails.root.to_s)
end
Rails.logger.warn(<<-END)
A reference to an obsolete attribute #{name} at:
#{filtered_backtrace.join("\n")}
END
end
end
इन परिवर्तनों के बाद, ऐप वही काम करता है लेकिन थोड़ा धीमा हो सकता है क्योंकि Profile के अतिरिक्त संदर्भों के कारण (यदि प्रदर्शन एक मुद्दा बन जाता है तो AppSignal जैसे टूल का उपयोग करें)। कोड लिगेसी विशेषताओं के सभी संदर्भों को लॉग करता है, यहां तक कि अन-ग्रेपेबल वाले (जैसे user[attr] = ... या user.send("#{attr}=", ...)) ताकि हम उन सभी का पता लगा सकें जब grep अनुपयोगी हो।
इस बुनियादी ढांचे के साथ, हम users.first_name के एक डिप्रिकेटेड संदर्भ को नियमित शेड्यूल पर ठीक करने के लिए प्रतिबद्ध हो सकते हैं, उदाहरण के लिए हर सुबह (दिन की शुरुआत एक त्वरित जीत के साथ करने के लिए) या दोपहर के आसपास (एक केंद्रित सुबह के बाद किसी आसान चीज़ पर काम करने के लिए)। यह प्रतिबद्धता आवश्यक है क्योंकि हमारा लक्ष्य इस मुद्दे को ठीक करने के लिए मानसिक बाधाओं को कम करना है। उपरोक्त कोड को बिना कार्रवाई किए छोड़ देने से ऐप और भी अधिक खराब होगा।
सभी डिप्रिकेटेड संदर्भों को हटाने के बाद (और grep और लॉग्स के साथ पुष्टि करने के बाद) हम अंततः users.first_name को हटा सकते हैं:
class RemoveUsersFirstName < ActiveRecord::Migration
def change
remove_column :users, :first_name, :string
end
end
हमें User में जोड़े गए कोड से भी छुटकारा पाना चाहिए क्योंकि यह अब आवश्यक नहीं है।
सीमाएं
यह विधि आपके मामले पर लागू हो सकती है लेकिन इसकी कुछ सीमाओं को ध्यान में रखें:
- यह
User.update_allजैसे बल्क क्वेरी को नहीं संभालता। - यह कच्चे SQL क्वेरी को नहीं संभालता।
- यह मंकी-पैच को तोड़ सकता है (याद रखें कि निर्भरताएं उन्हें भी पेश कर सकती हैं)।
UserऔरProfileआउट ऑफ सिंक हो सकते हैं, यदिprofiles.first_nameअपडेट किया गया है लेकिनusers.first_nameनहीं।
आप उनमें से कुछ को दूर करने में सक्षम हो सकते हैं। उदाहरण के लिए, आप एक सर्विस ऑब्जेक्ट या Profile पर कॉलबैक के साथ मॉडल को सिंक में रख सकते हैं। या यदि आप PostgreSQL का उपयोग करते हैं तो आप अंतरिम में एक मैटेरियलाइज़्ड व्यू का उपयोग करने पर विचार कर सकते हैं।
बस इतना ही!
लेख का सबसे महत्वपूर्ण सबक गंध वाले कोड से बचें नहीं बल्कि इसे सीधे निपटें। यदि कार्य भारी है तो नियमित शेड्यूल पर पुनरावृत्तियों से काम करें। लेख ने एक टेबल निकालते समय विचार करने के लिए एक विधि प्रस्तुत की। यदि आप इसे लागू नहीं कर सकते तो कुछ और खोजें। यदि आपको कोई अंदाजा नहीं है कि कैसे तो बस मुझे एक लाइन ड्रॉप करें। मैं मदद करने की कोशिश करूंगा। अपने बिट्स को सड़ने न दें।