কম্পিউটার টিউটোরিয়াল

Rails-এ ক্রস-ক্লাস্টার অ্যাসোসিয়েশন মাস্টার করুন: ডাটাবেস পার্টিশনিং চ্যালেঞ্জ জয় করুন

Rails ফ্রেমওয়ার্কের সুন্দর দিকগুলোর একটি হলো আপনার মডেলে Ruby on Rails অ্যাসোসিয়েশন ব্যবহার করার ক্ষমতা। এই অ্যাসোসিয়েশনগুলো আপনাকে আপনার কোডে রেকর্ডের কালেকশন অ্যাক্সেস করতে দেয় সुखদ সিনট্যাক্সে, আন্ডারলাইং SQL কোয়েরি লিখার প্রয়োজন দূর করে। সেই এবস্ট্রাকশন তখন পর্যন্ত কাজ করে যতক্ষণ না আপনার সारा ডেটা এক জায়গায় থাকে। আপনার টেবিলগুলো আলাদা ডাটাবেস ক্লাস্টারে ছড়িয়ে পড়ার মুহূর্তে, কিছু অ্যাসোসিয়েশন টাইপ কাজ করা বন্ধ করে দেয়।

এই আর্টিকেলে সঠিকভাবে সেটি কোথায় হয় এবং সীমার মধ্যে কাজ করার জন্য Rails কী প্রদান করে সেটি কভার করা হয়েছে। আমরা শুরু করব কেন সমস্যাটি হয় এবং Rails-এ কোন অ্যাসোসিয়েশনগুলো প্রভাবিত হয়, এবং ডাটাবেস কনফিগারেশন এবং মডেল হায়ারার্কি যেটি একাধিক ক্লাস্টার এবং মেনি-টু-মেনি রিলেশনশিপ সাপোর্ট করে সেটি নিয়ে যাব। তারপর আমরা দেখব কীভাবে বিভিন্ন ডেটা অ্যাক্সেস প্যাটার্ন সেটি ডিকম্পোজিশনের সাথে ইন্টারঅ্যাক্ট করে।

আপনি যদি মাল্টি-ডাটাবেস সেটআপ স্পেসিফিক্যালি কভার করে একটি Rails অ্যাসোসিয়েশন টিউটোরিয়াল খুঁজছেন, তবে এটি সেটি। আমরা এছাড়া অনেক কয়েকটা জিনিস আলোচনা করব, তাই থাকুন।

কেন ডাটাবেস আলাদা ক্লাস্টারে চলে যায়

যখন একটি Rails অ্যাপ্লিকেশন তার সকল ডেটা একটি সিঙ্গেল ডাটাবেসে স্টোর করে, Active Record অ্যাসোসিয়েশন ট্রান্সপারেন্টলি হ্যান্ডেল হয়, এবং আপনি আন্ডারলাইং SQL-র বিষয়ে কখনো চিন্তা করেন না। আপনার ডেটা একাধিক ডাটাবেস ক্লাস্টার জুড়ে ফেলার মুহূর্তে, সেই ট্রান্সপারেন্সি ভাঙে যায়। একটি JOIN রিকোয়ায়ার করে দুটি টেবিল একই ডাটাবেস সার্ভারে এক্সিস্ট করে। ক্লাস্টার জুড়ে JOIN আটেম্পট করলে ActiveRecord::StatementInvalid এরর 던져 যেন:

ActiveRecord::StatementInvalid (Table 'people_cluster.humans' doesn't exist) 

এটি কোনো কনফিগারেশন মিস্টেক নয়। এটি একটি হার্ড ফিজিক্যাল কনস্ট্রেইন্ট: ডাটাবেস সার্ভার সেই টেবিলদের সাথে JOIN করতে পারে না যেগুলো সেটি হোস্ট করে না। এই সমস্যাটি আছে has_many :through এবং has_one :through অ্যাসোসিয়েশনে, কারণ এগুলোই সেই অ্যাসোসিয়েশন টাইপ যারা ইন্টারমিডিয়েট JOIN কোয়েরি জেনারেট করে। ডায়রেক্ট has_many বা belongs_to রিলেশনশিপ JOIN রিকোয়ায়ার করে না তাই কোনো মডিফিকেশন ছাড়াই ক্লাস্টার জুড়ে কাজ করে।

কখন আপনি এই বাউন্ডারি হিট করবেন সেটি বুঝতে পারা প্রথম ধাপ। যদি একটি User accounts ডাটাবেসে থাকে এবং একটি Post content ডাটাবেসে থাকে, User has_many :posts খুব ভালো কাজ করে। কিন্তু আপনি যদি একটি ইন্টারমিডিয়েট Subscription মডেল billing ডাটাবেসে যোগ করুন এবং ডিফাইন করুন User has_many :posts, through: :subscriptions, Rails subscriptions এবং posts-কে একটি সিঙ্গেল কোয়েরিতে JOIN করার চেষ্টা করবে। সেটিই হলো যেখানে ক্লাস্টার বাউন্ডারি সমস্যা হয়।

থ্রি-টিয়ার ডাটাবেস কনফিগারেশন

যেকোনো মডেল কোড লিখার আগে, ডাটাবেস কনফিগারেশনটিকে মাল্টি-ক্লাস্টার লেআউট রিফ্লেক্ট করতে হবে। Rails config/database.yml-এ এই উদ্দেশ্যে একটি থ্রি-টিয়ার স্ট্রাকচার ব্যবহার করে। প্রতিটি টপ-লেভেল এনভায়রনমেন্ট কী নেস্টেড ডাটাবেস নাম ধারণ করে, এবং প্রতিটি সেটি সেই ক্লাস্টারের কানেকশন ডিটেইল ধারণ করে।

# config/database.yml default: &default adapter: postgresql pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: primary: <<: *default database: myapp_primary_dev accounts: <<: *default database: myapp_accounts_dev migrations_paths: db/accounts_migrate content: <<: *default database: myapp_content_dev migrations_paths: db/content_migrate production: primary: <<: *default database: myapp_primary_prod username: <%= ENV['DB_USER'] %> password: <%= ENV['DB_PASSWORD'] %> accounts: <<: *default database: myapp_accounts_prod username: <%= ENV['DB_USER'] %> password: <%= ENV['DB_PASSWORD'] %> content: <<: *default database: myapp_content_prod username: <%= ENV['DB_USER'] %> password: <%= ENV['DB_PASSWORD'] %> 

migrations_paths কী নন-অপশনাল যদি আপনি চান Rails জেনারেটর এবং db:migrate মাইগ্রেশনগুলো সঠিক ডিরেক্টরিতে রুট করে। এর বিনা, সব মাইগ্রেশন ডিফল্টভাবে db/migrate-এ যাবে এবং প্রাইমারি ডাটাবেসে অ্যাপ্লাই হবে। প্রতিটি সেকেন্ডারি ডাটাবেসেরও একটি করেসপন্ডিং অ্যাবস্ট্র্যাক্ট রেকর্ড ক্লাস থাকা উচিত যা Rails মডেল ইনহেরিট করে। জেনারেটর এটি অটোমেটিক্যালি হ্যান্ডেল করে যখন আপনি --database ফ্ল্যাগ পাস করেন:

rails generate model Subscription plan:string --database accounts 

এটি একটি AccountsRecord ক্লাস প্রডিউস করবে যদি ইতিমধ্যে এক্সিস্ট না করে, এবং জেনারেটেড Subscription মডেল সেটি ইনহেরিট করবে।

অ্যাবস্ট্র্যাক্ট রেকর্ড ক্লাস এবং কানেকশন রাউটিং

অ্যাবস্ট্র্যাক্ট রেকর্ড ক্লাসগুলো হলো Rails যে মেকানিজম ব্যবহার করে কোয়েরিকে সঠিক ক্লাস্টারে রাউট করার জন্য। প্রতিটি connects_to কল করে ডিক্লেয়ার করে যে কোন ডাটাবেস সেটি রাইটিং এবং রিডিং অপারেশনের জন্য ম্যাপ করে। আপনার অ্যাপ্লিকেশনটিতে সাধারণত এই হায়ারার্কিতে তিনটি লেয়ার থাকবে।

# app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to database: { writing: :primary, reading: :primary } end 
# app/models/accounts_record.rb class AccountsRecord < ApplicationRecord self.abstract_class = true connects_to database: { writing: :accounts, reading: :accounts } end 
# app/models/content_record.rb class ContentRecord < ApplicationRecord self.abstract_class = true connects_to database: { writing: :content, reading: :content } end 

User মডেল এই হায়ারার্কি বুঝতে একটি ভালো আ্কার। এটি accounts ক্লাস্টারে থাকে এবং AccountsRecord- থেকে ইনহেরিট করে। content ক্লাস্টারের মডেলগুলো ContentRecord- থেকে ইনহেরিট করে। বাকি সব ApplicationRecord- থেকে ইনহেরিট করে এবং প্রাইমারি ডাটাবেসে হিট করে। এই ইনহেরিটেন্স চেইন হলো Active Record কীভাবে নির্ধারণ করে কোন কানেকশন পুল ব্যবহার করবে কোয়েরি এক্সিকিউট করার সময়। এটি ক্লাস হায়ারার্কিয়ের উপরে চলে যতক্ষণ না একটি ক্লাস পায় যেটি connects_to কল করেছে। Rails-এ ক্রস-ক্লাস্টার অ্যাসোসিয়েশন মাস্টার করুন: ডাটাবেস পার্টিশনিং চ্যালেঞ্জ জয় করুন

একটি সাধারণ ভুল হলো ইন্ডিভিজুয়াল মডেলে establish_connection কল করা অ্যাবস্ট্র্যাক্ট ক্লাস ব্যবহার করার বদলে। প্রতিটি establish_connection কল একটি আলাদা কানেকশন পুল খুলে। আপনার যদি accounts ডাটাবেসে ৫০টি মডেল থাকে, প্রতিটি establish_connection কল করলে, আপনি একই সার্ভারে পয়েন্ট করে ৫০টি কানেকশন পুল দিয়ে ফেলবেন। অ্যাবস্ট্র্যাক্ট ক্লাস এই সমস্যাটি সমাধান করে একটি সিঙ্গেল পুল শেয়ার করে সকল মডেল যা সেটি থেকে ইনহেরিট করে।

Rails-এ ক্রস-ক্লাস্টার অ্যাসোসিয়েশন কীভাবে কাজ করে

disable_joins: true অপশন হলো ডায়রেক্ট মেকানিজম through অ্যাসোসিয়েশন কাজ করাতে যetanদিকের টেবিলগুলো আলাদা ক্লাস্টারে থাকে। Rails has_many সবচেয়ে কমনলি ব্যবহৃত অ্যাসোসিয়েশন টাইপ, এবং এটি সেটি যা ক্লাস্টার বাউন্ডারি দ্বারা সবচেয়ে ডায়রেক্টলি প্রভাবিত হয়। যখন Rails এই অপশনটি একটি অ্যাসোসিয়েশনে এনকাউন্টার করে, এটি সিঙ্গেল JOIN কোয়েরি স্ট্র্যাটেজি ছাড়িয়ে দেয় এবং বদলে দুইটি (বা অধিক) সিকোয়েনশিয়াল SELECT স্টেটমেন্ট ইস্যু করে, প্রথম কোয়েরি থেকে IDs পাইপ করে দ্বিতীয় কোয়েরির WHERE ... IN (...) ক্লজে।

এখানে তিনটি ক্লাস্টারে फैলে একটি কনক্রিট মডেল সেটআপ। নিচের মডেল সেটআপটি একটি মেনি-টু-মেনি রিলেশনশিপ, একটি User Subscriptions-এর মাধ্যমে Posts-এর সাথে কানেক্টেড, এবং এটি সেই প্যাটার্ন যা সবচেয়ে ডায়রেক্টলি ক্রস-ক্লাস্টার সমস্যা এক্সপোজ করে।

# app/models/user.rb - accounts ডাটাবেসে থাকে class User < AccountsRecord has_many :subscriptions has_many :posts, through: :subscriptions, disable_joins: true end 
# app/models/subscription.rb - accounts ডাটাবেসে থাকে class Subscription < AccountsRecord belongs_to :user has_many :posts end 
# app/models/post.rb - content ডাটাবেসে থাকে class Post < ContentRecord belongs_to :subscription end 

যখন আপনি user.posts কল করেন, Rails এই কোয়েরি পেয়ার জেনারেট করে একটি সিঙ্গেল JOIN-এর বদলে:

-- Query ১: accounts ক্লাস্টার থেকে সাবস্ক্রিপশন IDs ফেচ করুন SELECT "subscriptions"."id" FROM "subscriptions" WHERE "subscriptions"."user_id" = 1 -- Query ২: content ক্লাস্টার থেকে সেগুলো IDs ব্যবহার করে পোস্ট ফেচ করুন SELECT "posts".* FROM "posts" WHERE "posts"."subscription_id" IN (4, 7, 12) 

প্রথম কোয়েরি accounts ডাটাবেসে রান হয় একটি প্রাইমারি কী কালেক্ট করতে। দ্বিতীয়টি content-এ রান হয়। Rails রিলেশনশিপ রেজলভ করে ফরেন কী ফলো করে, user_id subscriptions-এ এবং subscription_id posts-এ, দুটি ক্লাস্টার জুড়ে। প্রথম কোয়েরি subscriptions থেকে প্রাইমারি কী ভ্যালু কালেক্ট করে, তারপর সেগুলো দ্বিতীয় কোয়েরির IN ক্লজে পাস করে। কোনো কোয়েরি ক্রস-ক্লাস্টার JOIN আটেম্পট করে না। Rails ফাইনাল রেজাল্ট সেট অ্যাপ্লিকেশন মেমরিতে অ্যাসেম্বল করে। Rails-এ ক্রস-ক্লাস্টার অ্যাসোসিয়েশন মাস্টার করুন: ডাটাবেস পার্টিশনিং চ্যালেঞ্জ জয় করুন Rails-এ ক্রস-ক্লাস্টার অ্যাসোসিয়েশন মাস্টার করুন: ডাটাবেস পার্টিশনিং চ্যালেঞ্জ জয় করুন একই অপশন has_one :through-এও একইভাবে কাজ করে:

# app/models/user.rb class User < AccountsRecord has_one :profile has_one :avatar, through: :profile, disable_joins: true end 
# app/models/profile.rb - accounts ডাটাবেস class Profile < AccountsRecord belongs_to :user has_one :avatar end 
# app/models/avatar.rb - content ডাটাবেস class Avatar < ContentRecord belongs_to :profile end 

user.avatar দুটি কোয়েরি এক্সিকিউট করবে: একটি profile_id পেতে, আরেকটি content ক্লাস্টার থেকে অবাতার রেকর্ড ফেচ করতে।

disable_joins কখন একস্প্লিসিটলি সেট করতে হবে

Rails অটোমেটিক্যালি ক্লাস্টার বাউন্ডারি ডিটেক্ট করে disable_joins ইনসার্ট করে না। Active Record-এ অ্যাসোসিয়েশন লোডিং লেজি। একটি অ্যাসোসিয়েশনের SQL সেটি মডেলে ডিক্লেয়ারড হয়ার পয়েন্টে নির্ধারিত হয়, না যে যখন সেটি আসলে ট্রিগার হয়। user.posts এক্সিকিউট হওয়ার সময়, Rails ইতিমধ্যে সিদ্ধান্ত নিয়ে ফেলেছে কিনা JOIN ব্যবহার করবে নাকি আলাদা কোয়েরি ব্যবহার করবে অ্যাসোসিয়েশন ডিক্লেয়ারেশন এর ওপর ভিত্তি করে।

এর মানে প্রতিটি through অ্যাসোসিয়েশন যা ক্লাস্টার বাউন্ডারি পার করে, সেটির ডিক্লেয়ারেশনে disable_joins: true থাকতে হবে।

আপনার মডেল অডিট করার একটি প্র্যাকটিক্যাল উপায় হলো যেকোনো through: অ্যাসোসিয়েশন খুঁজে বের করা যেখানെ সোর্স মডেল এবং টার্গেট মডেল আলাদা অ্যাবস্ট্র্যাক্ট রেকর্ড ক্লাস থেকে ইনহেরিট করে। যদি User < AccountsRecord এবং Post < ContentRecord, তবে has_many :posts, through: :subscriptions-এ disable_joins: true থাকতে হবে Subscription কোথায় থাকে সেটি নির্বিশেষে।

ক্লাস্টার জুড়ে ইগার লোডিং

disable_joins অপশনটি কীভাবে অ্যাসোসিয়েশন লোড হয় সেটি প্রভাবিত করে, কিন্তু এটি ক্রস-ক্লাস্টার ডেটার সাথে ইগার লোডিং স্ট্র্যাটেজি কীভাবে ইন্টারঅ্যাক্ট করে সেটি চেঞ্জ করে না। এই পার্থক্য বুঝতে হলে মাল্টি-ডাটাবেস সেটআপে N+1 কোয়েরি এড়াতে পারবেন।

eager_load ক্রস-ক্লাস্টার অ্যাসোসিয়েশনের জন্য অফ দ্য টেবিল। এটি একটি LEFT OUTER JOIN জেনারেট করে, যেটির সেম ফিজিক্যাল লিমিটেশন রয়েছে যেন একটি রেগুলার JOIN – দুটো টেবিল একই সার্ভারে থাকতে হবে। আপনি যদি আটেম্পট করেন User.eager_load(:posts) যেখানে পোস্টগুলো আলাদা ক্লাস্টারে থাকে, আপনি একই StatementInvalid এরর পাবেন।

preload সঠিক স্ট্র্যাটেজি। এটি প্রতিটি অ্যাসোসিয়েশনের জন্য আলাদা কোয়েরি ইস্যু করে এবং রিলেশনশিপ Ruby-তে অ্যাসেম্বল করে। এটি স্ট্রাকচারালি একই যেটি disable_joins একটি সিঙ্গেল রেকর্ডের জন্য করে। পার্থক্য হলো স্কেল: preload সেকেন্ড কোয়েরিটি সব লোডেড প্যারেন্ট রেকর্ড জুড়ে ব্যাচ করে।

# এটি ক্লাস্টার জুড়ে কাজ করে। # Query ১: SELECT "users".* FROM "users" # Query ২: SELECT "posts".* FROM "posts" WHERE "posts"."subscription_id" IN (...) users = User.preload(:posts).all users.each do |user| user.posts.each { |post| puts post.title } # কোনো অ্যাডিশনাল কোয়েরি ফায়ার হয় না end 

includes কাজ করবে在那些 কেসগুলিতে যেখানে এটি ইন্টারনালি preload-এ ডেলিগেট করে, যা এটি ডিফল্টভাবে করে যখন এসোসিয়েটেড টেবিলের কলাম রেফারেন্স করার কোনো কন্ডিশন নেই। আপনি যদি একটি .where ক্লজ যোগ করেন যা এসোসিয়েটেড টেবিলের কলামকে টাচ করে, includes eager_load বিহেভিয়ারে স্যুইচ করে, এবং ক্লাস্টার জুড়ে ফেল করবে। যেহেতু কোনো সন্দেহ থাকে includes কোন স্ট্র্যাটেজি বেছে নেবে, একস্প্লিসিটলি preload ব্যবহার করুন।

# includes ডেলিগেট করে preload-এ, ক্লাস্টার জুড়ে কাজ করে User.includes(:posts).all # includes স্যুইচ করে eager_load-এ where ক্লজের কারণে, ক্লাস্টার জুড়ে ফেল করে User.includes(:posts).where("posts.published = ?", true) # ক্লাস্টার জুড়ে ফিল্টারিং-এর জন্য preload + আলাদা where ব্যবহার করুন User.preload(:posts).all.select { |u| u.posts.any?(&:published?) } # অথবা লোড করার পর অ্যাপ্লিকেশন কোডে ফিল্টার করুন 

স্কোপড অ্যাসোসিয়েশন এবং ক্রস-ক্লাস্টার ফিল্টারিং

মাল্টি-ডাটাবেস সেটআপে আরও একটি সাবল ইন্টারঅ্যাকশন হলো স্কোপড অ্যাসোসিয়েশন। যখন আপনি একটি স্কোপ ডিফাইন করেন একটি has_many-এ যা ক্লাস্টার পার করে, স্কোপের SQL টার্গেট ডাটাবেসে রান করে, সোর্সে না।

class User < AccountsRecord has_many :subscriptions has_many :published_posts, -> { where(published: true) }, through: :subscriptions, source: :posts, class_name: "Post", disable_joins: true end 

where(published: true) ক্লজটি দ্বিতীয় কোয়েরিতে অ্যাপেন্ড হয়ে যায়, যেটি content ডাটাবেসে রান হয়। এটি সঠিক বিহেভিয়ার, এবং এর মানে আপনার স্কোপগুলো টার্গেট টেবিলের কলাম রেফারেন্স করতে পারে কোনো সমস্যা ছাড়া। যা আপনি করবেন না সেটি হলো ইন্টারমিডিয়েট টেবিলের কলাম সেটি স্কোপে রেফারেন্স করা, কারণ ইন্টারমিডিয়েট কোয়েরি ইতিমধ্যে কমপ্লিট হয়ে গেছে তখন স্কোপড কোয়েরি এক্সিকিউট হয়।

# এটি ফেল করবে কারণ subscriptions.active content ডাটাবেসে একটি কলাম নয় has_many :active_posts, -> { where("subscriptions.active = ?", true) }, through: :subscriptions, source: :posts, disable_joins: true 

ইন্টারমিডিয়েট রেকর্ড ফিল্টার করুন ইন্টারমিডিয়েট অ্যাসোসিয়েশনে একটি স্কোপ যোগ করে:

class User < AccountsRecord has_many :active_subscriptions, -> { where(active: true) }, class_name: "Subscription" has_many :active_posts, through: :active_subscriptions, source: :posts, disable_joins: true end 

এখন subscriptions.active-এ ফিল্টারিং প্রথম কোয়েরিতে হয়, accounts ডাটাবেসে, এবং শুধু এক্টিভ সাবস্ক্রিপশন থেকে IDs দ্বিতীয় কোয়েরিতে পাস হয়।

হরিজোন্টাল শার্ডিং এবং ক্রস-শার্ড অ্যাসোসিয়েশন

একটি লজিক্যাল ডাটাবেসকে পার্টিশন কী যেমন tenant_id দিয়ে একাধিক সার্ভারে স্প্লিট করা ক্রস-ক্লাস্টার সমস্যার একটি সেকেন্ড ডাইমেনশন যুক্ত করে। disable_joins মেকানিজম এখনও প্রযোজ্য, কিন্তু কানেকশন রাউটিং আরও জটিল হয়।

Rails connected_to প্রদান করে রিকোয়েস্টের ভিতরে শার্ড স্যুইচ করার জন্য:

ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do User.find(1) # Hits shard_one end 

যখন অ্যাসোসিয়েশন দুটি ক্লাস্টার এবং শার্ড উভয়েই फैলে, আপনাকে নিশ্চিত করতে হবে শার্ড কনটেক্সট এবং disable_joins অপশন দুটোই inplace আছে। shard_one-এ একটি User আলাদা content ডাটাবেসে থাকা পোস্ট অ্যাক্সেস করলে সেটি একই দুই-কোয়েরি ডিকম্পোজিশন ব্যবহার করবে।

Rails ৮ ইন্ট্রোস্পেকশন মেথড যোগ করেছে যা রানটাইমে শার্ড টপোলজি সম্পর্কে রিজনিং সহজ করে দেয়:

class ShardedBase < ActiveRecord::Base self.abstract_class = true connects_to shards: { shard_one: { writing: :shard_one }, shard_two: { writing: :shard_two } } end class User < ShardedBase; end User.shard_keys # => [:shard_one, :shard_two] User.sharded? # => true ShardedBase.connected_to_all_shards do User.current_shard # Yields :shard_one, then :shard_two end 

connected_to_all_shards বিশেষভাবে ব্যাকগ্রাউন্ড জবের জন্য উপযোগী যেগুলো সব শার্ড জুড়ে রেকর্ড প্রসেস করতে চায়। এটি প্রতিটি শার্ড ক्रमাগতভাবে ইটারেট করে, প্রতিটি ব্লক এক্সিকিউশনের জন্য কানেকশন কনটেক্সট স্যুইচ করে।

টেন্যান্ট-ভিত্তিক শার্ডিং-এ, শার্ড স্যুইচিং-এ lock: true ডিফল্ট আকস্মিক টেন্যান্ট হপিং রোধ করে মিড-রিকোয়েস্ট। এটি একটি সেফটি মেকানিজম: একবার একটি রিকোয়েস্ট একটি টেন্যান্টের শার্ডে রুট হলে, অ্যাপ্লিকেশন কোড সেটিকে অন্য টেন্যান্টের শার্ডে স্যুইচ করতে পারবে না একস্প্লিসিটলি lock: false পাস করা ছাড়া। একটি সিঙ্গেল টেন্যান্টের শার্ডের ভিতরে ক্রস-ক্লাস্টার অ্যাসোসিয়েশন এখনও disable_joins ব্যবহার করে সেই অ্যাসোসিয়েশনগুলোর জন্য যেগুলো আলাদা ডাটাবেস ক্লাস্টার টাচ করে।

ক্রস-ক্লাস্টার অ্যাসোসিয়েশন টেস্ট করা

মাল্টি-ডাটাবেস সেটআপ টেস্ট করার জন্য আপনার টেস্ট এনভায়রনমেন্ট প্রোডাকশন ডাটাবেস টপোলজি মিরর করতে হবে। Rails-এর টেস্ট ফ্রেমওয়ার্ক এটিকে সাপোর্ট করে, কিন্তু কনফিগারেশন একস্প্লিসিট হতে হবে।

database.yml-এ প্রতিটি ডাটাবেসের একটি test এনভায়রনমেন্ট ব্লক থাকতে হবে। ফিক্সচার এবং ফ্যাক্টরি-ভিত্তিক টেস্ট ডেটা সঠিক ডাটাবেস টার্গেট করতে হবে। যদি একটি User ফ্যাক্টরি accounts ডাটাবেসে একটি রেকর্ড তৈরি করে এবং একটি Post ফ্যাক্টরি content-এ একটি তৈরি করে, তাদের মধ্যে অ্যাসোসিয়েশন শুধু তখন কাজ করবে যদি দুটি রেকর্ড তাদের রেসপেক্টিভ ডাটাবেসে একই টেস্ট ট্রানজ্যাকশনের ভিতরে এক্সিস্ট করে।

Rails প্রতিটি টেস্টকে ডিফল্টভাবে একটি ট্রানজ্যাকশনে র‍্যাপ করে, কিন্তু সেই ট্রানজ্যাকশনটি পের-কানেকশন। একাধিক ডাটাবেসের সাথে, প্রতিটি কানেকশন তার নিজস্ব ট্রানজ্যাকশন পায়। এর মানে টেস্ট ক্লিনআপ (প্রতিটি টেস্টের শেষে অটোমেটিক রোলব্যাক) প্রতিটি ডাটাবেসে مستقلভাবে হয়। আপনার টেস্ট যদি একটি User accounts-এ লিখে এবং একটি Post content-এ লিখে, দুটোই রোলব্যাক হবে, কিন্তু শুধু যদি টেস্ট ফ্রেমওয়ার্ক দুটি কানেকশন সম্পর্কে জানে।

fixtures ডিক্লেয়ারেশন এটি অটোমেটিক্যালি হ্যান্ডেল করে যখন মডেলগুলো সঠিক অ্যাবস্ট্র্যাক্ট ক্লাস থেকে ইনহেরিট করে। ফ্যাক্টরি-ভিত্তিক সেটআপের জন্য (FactoryBot, Fabricator), নিশ্চিত করুন প্রতিটি ফ্যাক্টরির create স্ট্র্যাটেজি সঠিক ডাটাবেস হিট করে মডেলের নিজস্ব connects_to রাউটিং দিয়ে।

# spec/factories/users.rb FactoryBot.define do factory :user do # User AccountsRecord থেকে ইনহেরিট করে এবং অটোমেটিক্যালি accounts DB-এ লিখে name { Faker::Name.name } end end # spec/factories/posts.rb FactoryBot.define do factory :post do # Post ContentRecord থেকে ইনহেরিট করে এবং অটোমেটিক্যালি content DB-এ লিখে association :subscription title { Faker::Lorem.sentence } end end 

ক্রস-ক্লাস্টার অ্যাসোসিয়েশন প্রত্যাশা অনুযায়ী কোয়েরি ফায়ার করছে কিনা তা ভেরিফাই করতে, sql.active_record নটিফিকেশন সাবস্ক্রাইব করুন:

# spec/support/query_counter.rb module QueryCounter def assert_query_count(expected, &block) count = 0 callback = ->(_name, _start, _finish, _id, payload) do count += 1 unless payload[:name] == "SCHEMA" || payload[:sql].start_with?("EXPLAIN") end ActiveSupport::Notifications.subscribed(callback, "sql.active_record", &block) assert_equal expected, count, "Expected #{expected} queries, got #{count}" end end 

একটি has_many :through with disable_joins: true একটি সিঙ্গেল রেকর্ডের জন্য একজাক্ট ২ কোয়েরি প্রডিউস করা উচিত। আপনি যদি ১ দেখেন, JOIN এখনও আটেম্পট করা হচ্ছে (এবং প্রোডাকশনে আলাদা সার্ভারের বিপরীতে ফেল করবে)। আপনি যদি N+1 দেখেন, ইগার লোডিং প্রত্যাশা অনুযায়ী কাজ করছে না।

কয়েকটি সতর্কতা

disable_joins অ্যাসোসিয়েশন লোডিং সমস্যাটি সমাধান করে, কিন্তু এটি কোয়েরি চেইনিং-এ এক্সটেন্ড করে না। আপনি .where, .order, বা .group ক্লজ চেইন করতে পারবেন না যেগুলো কলাম রেফারেন্স করে ক্লাস্টার জুড়ে একটি সিঙ্গেল Active Record রিলেশনে:

# এটি কাজ করবে না, আপনি ক্লাস্টার জুড়ে অর্ডার কলাম দিয়ে প্রডাক্ট ফিল্টার করতে পারবেন না customer.purchased_products.where("orders.total > ?", 100) 

কোয়েরি যেগুলো একাধিক ক্লাস্টারের ডেটা দিয়ে ফিল্টার বা সোর্ট করতে চায়, সেগুলো ম্যানুয়ালি ডিকম্পোজ করুন। এক ক্লাস্টার থেকে আপনার প্রয়োজনীয় IDs বা ভ্যালু ফেচ করুন, তারপর সেগুলো অন্য ক্লাস্টারের বিরুদ্ধে একটি কোয়েরির ইনপুট হিসেবে ব্যবহার করুন:

high_value_order_ids = Order.where(customer_id: customer.id) .where("total > ?", 100) .pluck(:id) line_item_product_ids = LineItem.where(order_id: high_value_order_ids).pluck(:product_id) products = Product.where(id: line_item_product_ids) 

এটি সেম ডিকম্পোজিশন যেটি disable_joins ইন্টারনালি পারফর্ম করে, কিন্তু একস্প্লিসিটলি করা হয়েছে যাতে আপনি প্রতিটি স্টেজে ফিল্টারিং অ্যাপ্লাই করতে পারেন। এটি বেশি ভার্বোস, কিন্তু এটি ক্লাস্টার বাউন্ডারি কোডে দৃশ্যমান করে রেলস সিনট্যাক্সের পিছনে সেগুলো লুকিয়ে রাখার বদলে।

Editor's note: This post was originally published in January 2023 and has been updated for accuracy.

  1. CSS3 কীফ্রেম অ্যানিমেশন তৈরি করার পদ্ধতি

  2. অ্যান্ড্রয়েড লিস্টভিউতে ইন্টিজার অ্যারে যোগ করা

  3. জাভাস্ক্রিপ্টে ডি-স্ট্রাকচারিং অ্যাসাইনমেন্ট

  4. HTML DOM ইনপুট সাবমিট form প্রপার্টি