ক্লায়েন্ট কীভাবে এই পরিস্থিতিতে পৌঁছেছে?
বিবরণে যাওয়ার আগে, চেষ্টা করি বুঝতে একটি অ্যাপ কীভাবে এই অবস্থায় পৌঁছাতে পারে। আমরা একটি সাধারণ users টেবিল দিয়ে শুরু করি। কয়েক সপ্তাহ পর, আমাদের শেষ সাইন-ইন সময় জানতে হয় তাই আমরা users.last_sign_in_at যোগ করি। তারপর ইউজারের নাম জানতে হয়। আমরা first_name এবং last_name যোগ করি। টুইটার হ্যান্ডেল? আরেকটি কলাম। গিটহাব প্রোফাইল? ফোন নম্বর? কয়েক মাস পর টেবিলটি অত্যন্ত জটিল হয়ে ওঠে।
এতে সমস্যা কী?
এত বড় একটি টেবিল বেশ কিছু সমস্যা নির্দেশ করে:
Userমডেলের একাধিক असংবদ্ধ দায়িত্ব রয়েছে। এতে এটি বোঝা, পরিবর্তন করা এবং পরীক্ষা করা কঠিন হয়।- অ্যাপ এবং ডাটাবেসের মধ্যে ডেটা এক্সচেঞ্জে অতিরিক্ত ব্যান্ডউইথ দরকার হয়।
- বল্কি মডেল সংরক্ষণে অ্যাপকে অধিক মেমরি দরকার হয়।
অ্যাপ প্রতিটি রিকোয়েস্টে প্রমাণীকরণ ও অনুমোদনের জন্য User ফেচ করত, কিন্তু সাধারণত কয়েকটি কলামই ব্যবহার করত। এই সমস্যা সমাধান করলে ডিজাইন ও পারফরম্যান্স উভয়েই উন্নত হবে।
একটি টেবিল Извлечение (Extracting a table)
বিরল ব্যবহৃত কলামগুলো নতুন এক বা একাধিক টেবিলে সরিয়ে এই সমস্যাটি সমাধান করা যায়। উদাহরণস্বরূপ, প্রোফাইল তথ্য (first_name ইত্যাদি) profiles টেবিলে নিচের ধাপগুলো অনুসরণ করে সরানো যায়:
users-এ প্রোফাইল-সম্পর্কিত কলামগুলো ডুপ্লিকেট করেprofilesতৈরি করুন।users-এprofile_idযোগ করুন। ফিলহাল এটিNULLরাখুন।users-এর প্রতিটি রোয়ের জন্য, প্রোফাইল-সম্পর্কিত কলামগুলো ডুপ্লিকেট করেprofiles-এ একটি রো ইনসার্ট করুন।- চতুর্থ ধাপে ইনসার্ট করা রোকে নির্দেশ করে
users-এ 대응하는 রোয়েরprofile_idসেট করুন। users.profile_id-কে non-NULLকরবেন না। অ্যাপ এখনও এর অস্তিত্ব জানা নেই, তাই এটি ভাঙবে।
এখন users.first_name এর রেফারেন্সগুলো profiles.first_name দিয়ে প্রতিস্থাপন করতে হবে। যদি কয়েকটি কলাম এবং হাতেকড়ে রেফারেন্স থাকে তবে ম্যানুয়ালি করা উচিত। কিন্তু যদি মনে হয় "ওহ, না! এটি সবচেয়ে বadrons কাজ!" তবে বিকল্প খুঁজুন।
সমস্যাকে অবহেলা করবেন না। যে কোড অংশটি সবাই এড়াতে চায় সেটি আরও অবনত হবে এবং আরও বেশি অভাবগ্রস্ত হবে। এই কুচক্র ভাঙার সবচেয়ে সহজ উপায় হল ছোট থেকে শুরু করা।
আরও জানতে পড়ুন, যদি আপনি কৌতূহলী হন যে আমার ক্লায়েন্ট কীভাবে সমস্যাটি সমাধান করেছেন।
কোড লাইন বাই লাইন ঠিক করা
সবচেয়ে ধাপে ধাপে পদ্ধতিটি হল একবারে পুরনো কলামের একটি রেফারেন্স ঠিক করা। চলুন 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) একটি অপ্রচলিত অ্যাট্রিবিউট #{name}-র রেফারেন্স: #{filtered_backtrace.join("\n")} END end endএই পরিবর্তনগুলো után অ্যাপ একইভাবে কাজ করবে কিন্তু এক্সট্রা Profile রেফারেন্সের কারণে কিছুটা ধীর হতে পারে (যদি পারফরম্যান্স সমস্যা হয় তবে AppSignal-এর মত টুল ব্যবহার করুন)। কোড লিগ্যাসি অ্যাট্রিবিউটের সকল রেফারেন্স লগ করে, এমনকি গ্রেপ-নাও যেগুলো (যেমন user[attr] = ... বা user.send("#{attr}=", ...))। ফলে গ্রেপ কাজ না করলেও সকল রেফারেন্স খুঁজে বের করা যাবে।
এই ইনফ্রাস্ট্রাকচার থাকলে, আমরা নিয়মিত সময়ে users.first_name-এর একটি ডিপ্রিকেটেড রেফারেন্স ঠিক করতে প্রতিশ্রুতি বদ্ধ হইতে পারি, যেমন প্রতি সকাল (দিন শুরুতে একটি দ্রুত জয়ের জন্য) বা দুপুরের দিকে (সुबহ ফোকাসড কাজের পর কিছু সহজ করার জন্য)। এই প্রতিশ্রুতি অপরিহার্য কারণ আমাদের লক্ষ্য হল সমস্যা ঠিক করার মানসিক বাধা কমানো। উপরের কোডটি যেন আছে কিন্তু কোনো কর্ম নেওয়া না হলে অ্যাপ আরও দরিদ্র হবে।
সকল ডিপ্রিকেটেড রেফারেন্স সরিয়ে ফেলার পর (ও grep এবং লগ দিয়ে নিশ্চিত হয়ে) আমরা অবশেষে users.first_name ড্রপ করতে পারি:
class RemoveUsersFirstName < ActiveRecord::Migration def change remove_column :users, :first_name, :string end endUser-এ যোগ করা কোডও সরিয়ে ফেলা উচিত কারণ সেটি আর দরকার নেই।
সীমাবদ্ধতা
এই পদ্ধতি আপনার কেসে প্রযোজ্য হতে পারে কিন্তু কিছু সীমাবদ্ধতা মনে রাখবেন:
- এটি বাল্ক কোয়েরি যেমন
User.update_allহ্যান্ডেল করে না। - এটি রস SQL কোয়েরি হ্যান্ডেল করে না।
- এটি মনকি-প্যাচ ভাঙতে পারে (মনে রাখবেন ডিপেন্ডেন্সিগুলোও এগুলো অনুরূপ করতে পারে)।
UserওProfileআউট-অফ-সিংক হতে পারে, যদিprofiles.first_nameআপডেট হয় কিন্তুusers.first_nameআপডেট না হয়।
আপনি কিছু সীমাবদ্ধতা অতিক্রম করতে পারেন। উদাহরণস্বরূপ, Profile-এ একটি সার্ভিস অবজেক্ট বা কলব্যাক দিয়ে মডেলগুলো সিঙ্কে রাখতে পারেন। অথবা PostgreSQL ব্যবহার করলে মধ্যবর্তী সময়ের জন্য ম্যাটেরিয়ালাইজড ভিউ বিবেচনা করতে পারেন।
শেষ কথা
এই আর্টিকেলের সবচেয়ে গুরুত্বপূর্ণ পাঠ হল দুর্গন্ধযুক্ত কোড এড়িয়ে চলা না, বরং সেটির মুখোমুখি হওয়া। যদি কাজটি অত্যধিক বড় মনে হয় তবে নিয়মিত সময়সূচী অনুযায়ী ইটেরেটিভলি কাজ করুন। আর্টিকেলটি একটি পদ্ধতি উপস্থাপন করেছে যেটি টেবিল এক্সট্র্যাকশন কঠিন হলে বিবেচনা করা যেতে পারে। যদি আপনি এটি প্রয়োগ না করতে পারেন তবে অন্য কিছু খুঁজুন। যদি কীভাবে করবেন সেটি সম্পর্কে কোনো ধারণা না থাকে তবে আমাকে একটা লাইন দিন। আমি সাহায্য করার চেষ্টা করব। আপনার বিটগুলো সڑাতে দিবেন না।