কখনো কখনো ইউনিক পরিস্থিতি এবং আমাদের নিয়ন্ত্রণ বাইরের জিনিসগুলো বাধ্য করে বاحية অনঅর্থডক্স (wildly unorthodox) রিকোয়ারমেন্টস মেনে নেওয়ায়। সম্প্রতি, আমার এমন একটি অভিজ্ঞতা হয়েছিল যেখানে আমাকে ডাটাবেস আইডি ছাড়া ActiveRecord ব্যবহার করতে হয়েছিল যেকোনো রেকর্ডের জন্য। কেউ যদি একই কিছু করার কথা ভাবে, আমি সেরিয়াসলি অন্য উপায় খুঁজে বের করার সুপারিশ করব! কিন্তু, চলুন গল্পের বাকি অংশে যাই।
ডিসিশন নেওয়া হয়েছিল। ছোট ডাটাবেসগুলো (স্ট্রাকচারে ক্লোন কিন্তু ডেটায় নয়) মার্জ করতে হতো। আমি প্রজেক্টে তখন জয়েন করলাম যখন টিম একটি স্ক্রিপ্টের ফাইনাল টাচ দিচ্ছিল যা একটি ডাটাবেস থেকে অন্যটিতে রেকর্ড কপি-পেস্ট করছিল। এটা সব কিছু এক্সাক্টলি কপি করছিল, আইডি সহ।
ডাটাবেস ক
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | orange | 456 |
| ... | ... | ... |
ডাটাবেস খ
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | banana | 74 |
| ... | ... | ... |
মার্জ পরে ডাটাবেস ক
| id | fruit | user_id |
|---|---|---|
| ... | ... | ... |
| 123 | orange | 456 |
| 123 | banana | 74 |
| ... | ... | ... |
এটি আইডি থাকার মূল কারণ ভাঙে দেয়: ইউনিক আইডেন্টিফিকেশন। আমি স্পেসিফিকস জানতাম না, কিন্তু লাগলো সব ধরনের সমস্যা দেখা যাবে একবার ডুপ্লিকেট আইডি সিস্টেমে ঢুকলে। আমি কিছু বলতে চেয়েছিলাম, কিন্তু আমি প্রজেক্টে নতুন, এবং অন্যদের মনে ছিল এটি বেস্ট পথ। কয়েক দিনে, আমরা কোড ডিপ্লয় করব এবং ডুপ্লিকেট আইডি সহ ডেটা হ্যান্ডেল শুরু করব। প্রশ্ন আর ছিল না, "আমরা কি করব?"; বরং, "আমরা কীভাবে করব?" এবং "এতে আর কতক্ষণ লাগবে?"
ডুপ্লিকেট আইডি সহ কাজ
তাহলে, ডুপ্লিকেট আইডি সহ ডেটা কীভাবে হ্যান্ডেল করবেন? সমাধান ছিল কম্পোজিট আইডি (composite ID) তৈরি করা বেশ কিছু ফিল্ড দিয়ে। আমাদের অধিকাংশ DB ফেচες এরকম দেখতো:
# এটি কাজ করবে না, ২টি ইউজার থাকতে পারে id: 123 সহ
FavoriteFruit.find(123)
# মাল্টিপল আইডি কুয়েরিকে সঠিক রেকর্ডে স্কোপ করে
FavoriteFruit.find_by(id: 123, user_id: 456)
সকল ActiveRecord কলগুলো এইভাবে আপডেট করা হয়েছিল, এবং কোডটি এক নজরে দেখলে মেনিংফুল লাগল। যতক্ষণ ডিপ্লয় করলাম না।
সর্বনাশ শুরু
কোড ডিপ্লয় করারitôt পর, ফোন বাজতে শুরু করল। কাস্টমাররা যেন না মিলে যাওয়া নম্বর দেখছিল। তারা তাদের নিজের রেকর্ড আপডেট করতে পারত না। সব ধরনের ফিচার ভাঙতে শুরু করল।
এখন কী করব? আমরা শুধু কোড ডিপ্লয় করিনি; আমরা ডেটা একটি ডাটাবেস থেকে অন্যটিতে মুভও করলাম (এবং নতুন ডেটা ডিপ্লয় করার পর তৈরি/আপডেট হয়েছিল)। এটি সিম্পল রোলব্যাক সিনেরিও নয়। আমাদের দ্রুত ফিক্স করতে হতো।
রেইলস কী করে?
ডিবাগিং-র প্রথম ধাপ ছিল বর্তমান আচরণ দেখা এবং কীভাবে এরর রিপ্রোডিউস করা যায়। আমি প্রোডাকশন ডেটার একটি ক্লোন নিয়ে রেইলস কনসোল শুরু করলাম। আপনার সেটআপের উপর নির্ভর করে, আপনি অটোমেটিক্যালি SQL কুয়েরিগুলো দেখতে পাবেন না যা রেইলস রান করে যখন আপনি ActiveRecord কুয়েরি এক্সিকিউট করেন। কনসোলে 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) এর একটি র্যাপার (wrapper)। শুধু আইডি দিয়ে কুয়েরি করা এই মেথডে হার্ডকোডেড। ডুপ্লিকেট আইডি সহ কাজ করার জন্য, আমাদের কোর রেইলস মেথড ওভাররাইড করতে হতো (কখনো রিকমেন্ড করা হয় না) অথবা 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-এর চেয়ে নিশ্চিতভাবে পড়া এবং বুঝতে কঠিন।
আসল সমাধান
সানকেন কস্ট (sunk costs), ম্যানেজমেন্ট, এবং টাইম কন্স্ট্রেইন্টস (time constraints) कारনে, আমাদের সমাধান ছিল রেইলসকে মンキ প্যাচ (monkey patch) করে মাল্টিপল কি ব্যবহার করা সব ডাটাবেস কলের জন্য। এটি কাজ করলো, অর্থাৎ কাস্টমাররা এখনও প্রোডাক্ট কিনে ব্যবহার করবেন, কিন্তু এটি অনেক কারণে খারাপ আইডিয়া:
- ভবিষ্যতের ডেভেলপমেন্ট অনাবশ্যে কমন রেইলস মেথড ব্যবহার করে বাগ রিইন্ট্রোডিউস করতে পারে। নতুন ডেভেলপারদের স্ট্রিক ট্রেনিং দরকার কোডকে হিডেন বাগ-মুক্ত রাখতে, যেমন
reloadমেথড ব্যবহার। - কোড আরও জটিল, কম স্পষ্ট, এবং কম মেইনটেইনযোগ্য। এটি টেকনিক্যাল ডেট যা ডেভেলপমেন্ট স্পিড ধীরে ধীরে কমিয়ে দেবে প্রজেক্ট চলার সাথে সাথে।
- টেস্টিং অনেক স্লো হয়ে যায়। আপনাকে শুধু ফাংশন কাজ করে কিনা সেটি না, বরং ডুপ্লিকেট আইডি থাকলে ও কাজ করে কিনা সেটি টেস্ট করতে হবে। টেস্ট লিখতে বেশি সময় লাগে, এবং টেস্ট স্যুট রান করতেও বেশি সময় লাগে। টেস্টিং আশানীতে বাগ মিস করতে পারে যদি প্রতিটি ডেভেলপার সাবধানে সব সিনেরিও টেস্ট না করে।
এই সমস্যার আসল সমাধান হলো প্রথমে কখনো ডুপ্লিকেট আইডি থাকা না দেওয়া। যদি ডেটা এক ডাটাবেস থেকে অন্যটিতে ট্রান্সফার করতে হয়, তবে সেটি করা স্ক্রিপ্টটি আইডি ছাড়া ডেটা কালেক্ট ও ইনসার্ট করা উচিত, যাতে রিসিভিং ডাটাবেস তার স্ট্যান্ডার্ড অটো-ইনক্রিমেন্ট কাউন্টার ব্যবহার করে প্রতিটি রেকর্ডকে তার নিজস্ব ইউনিক আইডি দেয়।
অন্য একটি সমাধান হলে সব রেকর্ডের জন্য UUID ব্যবহার করা। এই টাইপের আইডি হলো র্যান্ডমলি তৈরি হওয়া লম্বা ক্যারেক্টার স্ট্রিং (স্টেপ-বাই-স্টেপ কাউন্টিং এর বদলে, যেন ইন্টিজার আইডির সাথে হয়)। তাহলে, ডেটা অন্য ডাটাবেসে মুভ করলে কোনো কনফ্লিক্ট বা ইস্যু হবে না।
নিচের লাইন হলো: রেইলস এই ধারণায় বিল্ট যে আইডি প্রতি রেকর্ডে ইউনিক এবং ডাটাবেসে স্পেসিফিক ডেটা ম্যানিপুলেট করার দ্রুত ও সহজ উপায়। রেইলস একটি অপিনিয়নেটেড (opinionated) ফ্রেমওয়ার্ক, এবং এর সৌন্দর্য হলো সব কিছু কত স্মুথলি চলে, যতক্ষণ না আপনি রেইলস পদ্ধতিতে চলে যান। এটি শুধু রেইলসের জন্য নয়, প্রোগ্রামিংয়ের অনেক দিকের জন্যও প্রযোজ্য। জিনিসগুলো জটিল হলে, আমাদের সমস্যাটি আইডেন্টিফাই করতে জানা উচিত; কিন্তু যদি আমরা ক্লিয়ার, মেইনটেইনযোগ্য, এবং কনভেনশনাল কোড লিখি, তবে আমরা এই জটিলতাগুলোর অনেকটা আগেই এড়াতে পারি।